@lakindu_perera/toren 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -62,6 +62,14 @@ toren --format markdown > PROJECT_REPORT.md
62
62
  toren --format html > report.html
63
63
  toren --format json > scan.json
64
64
 
65
+ # Focused Output Modes (Mutually Exclusive)
66
+ toren --project-type
67
+ toren --frameworks
68
+ toren --entry-points
69
+ toren --structure
70
+ toren --configs
71
+ toren --scripts
72
+
65
73
  # Lifecycle & Help Commands
66
74
  toren --help
67
75
  toren --version
@@ -74,6 +82,12 @@ toren --uninstall
74
82
  | Flag | Description |
75
83
  |------|-------------|
76
84
  | `[path]` | Directory to scan. Defaults to the current directory (`.`). |
85
+ | `--project-type` | Show detected project type only. |
86
+ | `--frameworks` | Show detected frameworks only. |
87
+ | `--entry-points` | Show detected entry points only. |
88
+ | `--structure` | Show repository structure only. |
89
+ | `--configs` | Show detected project configuration files. |
90
+ | `--scripts` | Show available package scripts only. |
77
91
  | `--format <type>` | Output format: `console` (default), `json`, `markdown`, `html`. |
78
92
  | `--include-hidden` | Include hidden files and dot-directories in the scan. |
79
93
  | `--max-files <N>` | Override the default 50,000-file scan limit. |
@@ -83,18 +97,48 @@ toren --uninstall
83
97
  | `--uninstall` | Safely remove Toren from the global npm environment. |
84
98
 
85
99
  > **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
100
+ > **Note:** Focused output flags (`--project-type`, `--frameworks`, `--entry-points`, `--structure`, `--configs`, `--scripts`) are mutually exclusive.
86
101
 
87
102
  ---
88
103
 
89
104
  ## Output Examples
90
105
 
106
+ ### Focused Output Example
107
+ Sometimes you only need a specific piece of intelligence for use in a script or a quick lookup. Use the focused output flags to bypass the full report:
108
+
109
+ ```bash
110
+ $ toren --project-type
111
+ Project Type: Node.js / JavaScript
112
+
113
+ $ toren --frameworks
114
+ Frameworks:
115
+ - React
116
+
117
+ $ toren --entry-points
118
+ Entry Points:
119
+ - src/main.tsx
120
+
121
+ $ toren --configs
122
+ Configuration Files
123
+ ───────────────────
124
+
125
+ ✓ package.json
126
+ ✓ vite.config.ts
127
+
128
+ $ toren --scripts
129
+ Available Scripts
130
+
131
+ start node bin/toren.js
132
+ test node --test
133
+ ```
134
+
91
135
  ### Console Output Example
92
136
  The default `console` format renders a beautiful summary directly in your terminal:
93
137
 
94
138
  ```text
95
- Toren v1.0.3 — Codebase Onboarding Intelligence
139
+ Toren v1.0.4 — Codebase Onboarding Intelligence
96
140
 
97
- 🔍 Project Summary
141
+ Project Summary
98
142
  ────────────────────────────────────────────────────────────────────────────────
99
143
  Path: ./my-react-app
100
144
  Project type: React
@@ -102,11 +146,11 @@ The default `console` format renders a beautiful summary directly in your termin
102
146
  Total folders: 6
103
147
  Scan duration: 4 ms
104
148
 
105
- 🚪 Entry Points
149
+ Entry Points
106
150
  ────────────────────────────────────────────────────────────────────────────────
107
151
  → src/main.tsx
108
152
 
109
- 📁 Folder Structure (first 20 files)
153
+ Folder Structure (first 20 files)
110
154
  ────────────────────────────────────────────────────────────────────────────────
111
155
  my-react-app/
112
156
  ├── public/
@@ -124,7 +168,7 @@ my-react-app/
124
168
  … and 21 more file(s) not shown
125
169
 
126
170
  ────────────────────────────────────────────────────────────────────────────────
127
- Scan complete.
171
+ Scan complete.
128
172
  ```
129
173
 
130
174
  ### JSON Output Example
@@ -209,12 +253,12 @@ Contributions to improve this codebase analyzer CLI are always welcome!
209
253
 
210
254
  ---
211
255
 
212
- ## 📄 License
256
+ ## License
213
257
 
214
258
  Distributed under the MIT License. See `LICENSE` for more information.
215
259
 
216
260
  ---
217
261
 
218
- ## 👨‍💻 Author
262
+ ## Author
219
263
 
220
264
  Built with by **[Lakindu Perera](https://github.com/lakindudev)**.
package/bin/toren.js CHANGED
@@ -25,6 +25,7 @@
25
25
  import { createRequire } from 'node:module';
26
26
  import { scan } from '../src/scanner/scan.js';
27
27
  import renderers from '../src/renderers/index.js';
28
+ import { getFocusedModeInfo, renderFocusedMode, FOCUSED_FLAGS } from '../src/focused-output.js';
28
29
  import { runDoctor, runUninstall } from '../src/lifecycle.js';
29
30
 
30
31
  const require = createRequire(import.meta.url);
@@ -44,14 +45,22 @@ function printHelp() {
44
45
  const formatList = SUPPORTED_FORMATS.map(f => ` ${f}`).join('\n');
45
46
  console.log(`
46
47
  \x1b[1mUsage:\x1b[0m
47
- toren [path] Scan a directory (defaults to current directory)
48
- toren --format <type> Select output format (default: console)
49
- toren --include-hidden Include hidden dot-files in the scan
50
- toren --max-files <N> Set max file scan limit (default: 50000)
51
- toren --help Show this help message
52
- toren --version Show version number
53
- toren --doctor Check global installation health
54
- toren --uninstall Safely remove global installation
48
+ toren [path] [options]
49
+
50
+ \x1b[1mOptions:\x1b[0m
51
+ --project-type Show detected project type only
52
+ --frameworks Show detected frameworks only
53
+ --entry-points Show detected entry points only
54
+ --structure Show repository structure only
55
+ --configs Show detected project configuration files
56
+ --scripts Show available package scripts only
57
+ --format <type> Output as console, json, markdown, or html
58
+ --include-hidden Include hidden files and folders
59
+ --max-files <n> Set scan file limit
60
+ --help Show this help message
61
+ --version Show version number
62
+ --doctor Run CLI diagnostics
63
+ --uninstall Remove Toren global installation
55
64
 
56
65
  \x1b[1mOutput Formats:\x1b[0m
57
66
  ${formatList}
@@ -140,6 +149,16 @@ function parseArgs() {
140
149
  format = 'json';
141
150
  }
142
151
 
152
+ // ── Focused Output Flags ────────────────────────────────────────────────
153
+ const focusedInfo = getFocusedModeInfo(args);
154
+
155
+ if (focusedInfo.error) {
156
+ console.error(focusedInfo.message);
157
+ return { action: 'exit', code: 1 };
158
+ }
159
+
160
+ const focusedMode = focusedInfo.mode;
161
+
143
162
  // ── Hidden files ────────────────────────────────────────────────────────
144
163
  const includeHidden = args.includes('--include-hidden');
145
164
 
@@ -184,6 +203,7 @@ function parseArgs() {
184
203
  '--uninstall',
185
204
  '--format',
186
205
  '--json',
206
+ ...FOCUSED_FLAGS,
187
207
  '--include-hidden',
188
208
  '--max-files',
189
209
  ]);
@@ -198,7 +218,7 @@ function parseArgs() {
198
218
  return { action: 'exit', code: 1 };
199
219
  }
200
220
 
201
- return { action: 'scan', target, format, includeHidden, maxFiles };
221
+ return { action: 'scan', target, format, includeHidden, maxFiles, focusedMode };
202
222
  }
203
223
 
204
224
  // ---------------------------------------------------------------------------
@@ -249,7 +269,12 @@ function assertValidFormat(format) {
249
269
  includeHidden: parsed.includeHidden,
250
270
  maxFiles: parsed.maxFiles,
251
271
  });
252
- render(result, { cwd: process.cwd() });
272
+
273
+ if (parsed.focusedMode) {
274
+ renderFocusedMode(parsed.focusedMode, result);
275
+ } else {
276
+ render(result, { cwd: process.cwd() });
277
+ }
253
278
  } catch (err) {
254
279
  // Render errors in the requested format where possible.
255
280
  if (parsed.format === 'json') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakindu_perera/toren",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A powerful codebase scanner CLI tool for project analysis, framework detection, and understanding repository structure instantly.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "start": "node bin/toren.js",
17
+ "test": "node --test",
17
18
  "lint": "node --check bin/toren.js src/scanner/scan.js src/lifecycle.js src/renderers/index.js src/renderers/console-renderer.js src/renderers/json-renderer.js src/renderers/markdown-renderer.js src/renderers/html-renderer.js"
18
19
  },
19
20
  "keywords": [
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @fileoverview Configuration file detector
3
+ */
4
+
5
+ /**
6
+ * Detects common configuration files in the root of the repository.
7
+ *
8
+ * @param {string[]} flatFiles - Array of relative file paths from the scanner
9
+ * @returns {{ configs: string[] }}
10
+ */
11
+ export function detectConfigs(flatFiles) {
12
+ const configPatterns = [
13
+ // Package
14
+ /^package\.json$/,
15
+ /^package-lock\.json$/,
16
+ /^pnpm-lock\.yaml$/,
17
+ /^yarn\.lock$/,
18
+ /^bun\.lockb$/,
19
+
20
+ // TypeScript
21
+ /^tsconfig\.json$/,
22
+ /^jsconfig\.json$/,
23
+
24
+ // Frontend
25
+ /^vite\.config\.[a-zA-Z0-9]+$/,
26
+ /^next\.config\.[a-zA-Z0-9]+$/,
27
+ /^nuxt\.config\.[a-zA-Z0-9]+$/,
28
+ /^astro\.config\.[a-zA-Z0-9]+$/,
29
+ /^angular\.json$/,
30
+
31
+ // Build
32
+ /^webpack\.config\.[a-zA-Z0-9]+$/,
33
+ /^rollup\.config\.[a-zA-Z0-9]+$/,
34
+ /^turbo\.json$/,
35
+ /^nx\.json$/,
36
+
37
+ // Styling
38
+ /^tailwind\.config\.[a-zA-Z0-9]+$/,
39
+ /^postcss\.config\.[a-zA-Z0-9]+$/,
40
+
41
+ // Lint
42
+ /^eslint\.config\.[a-zA-Z0-9]+$/,
43
+ /^\.eslintrc.*$/,
44
+ /^prettier\.config\.[a-zA-Z0-9]+$/,
45
+ /^\.prettierrc.*$/,
46
+
47
+ // Backend
48
+ /^pom\.xml$/,
49
+ /^build\.gradle$/,
50
+ /^build\.gradle\.kts$/,
51
+
52
+ // Languages
53
+ /^requirements\.txt$/,
54
+ /^pyproject\.toml$/,
55
+ /^Cargo\.toml$/,
56
+ /^go\.mod$/,
57
+
58
+ // Deployment
59
+ /^Dockerfile$/,
60
+ /^docker-compose\.ya?ml$/
61
+ ];
62
+
63
+ const matchedSet = new Set();
64
+
65
+ for (const file of flatFiles) {
66
+ // Ensure we only match files at the root of the repository
67
+ if (file.includes('/')) continue;
68
+
69
+ for (const pattern of configPatterns) {
70
+ if (pattern.test(file)) {
71
+ matchedSet.add(file);
72
+ break; // Stop checking patterns once a match is found
73
+ }
74
+ }
75
+ }
76
+
77
+ // Convert Set to Array and sort alphabetically
78
+ const configs = Array.from(matchedSet).sort();
79
+
80
+ return { configs };
81
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Detects npm/package scripts from package.json in the project root.
6
+ *
7
+ * @param {string} rootPath - The absolute path of the scanned root
8
+ * @returns {{ scripts: Array<{name: string, command: string}> }}
9
+ */
10
+ export function detectScripts(rootPath) {
11
+ const scripts = [];
12
+ const pkgPath = path.join(rootPath, 'package.json');
13
+
14
+ try {
15
+ if (fs.existsSync(pkgPath)) {
16
+ const content = fs.readFileSync(pkgPath, 'utf8');
17
+ const pkg = JSON.parse(content);
18
+
19
+ if (pkg.scripts && typeof pkg.scripts === 'object') {
20
+ for (const [name, command] of Object.entries(pkg.scripts)) {
21
+ if (typeof command === 'string') {
22
+ scripts.push({ name, command });
23
+ }
24
+ }
25
+ }
26
+ }
27
+ } catch (error) {
28
+ // Return empty scripts on malformed or unreadable package.json
29
+ }
30
+
31
+ return { scripts };
32
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Focused output modes handling for Toren CLI.
3
+ */
4
+
5
+ import { renderStructure } from './renderers/console-renderer.js';
6
+
7
+ export const FOCUSED_FLAGS = [
8
+ '--project-type',
9
+ '--frameworks',
10
+ '--entry-points',
11
+ '--structure',
12
+ '--configs',
13
+ '--scripts'
14
+ ];
15
+
16
+ /**
17
+ * Validates focused flags in the provided arguments.
18
+ *
19
+ * @param {string[]} args Process arguments
20
+ * @returns {{ error: boolean, message?: string, mode?: string|null }}
21
+ */
22
+ export function getFocusedModeInfo(args) {
23
+ const activeFlags = FOCUSED_FLAGS.filter(flag => args.includes(flag));
24
+
25
+ if (activeFlags.length > 1) {
26
+ return {
27
+ error: true,
28
+ message: '\x1b[31mError: focused output flags are mutually exclusive. Please use only one of:\x1b[0m\n' +
29
+ FOCUSED_FLAGS.map(f => ` ${f}`).join('\n') + '\n'
30
+ };
31
+ }
32
+
33
+ return {
34
+ error: false,
35
+ mode: activeFlags[0] || null
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Renders the scan result based on the active focused mode.
41
+ *
42
+ * @param {string} mode Active focused mode flag
43
+ * @param {import('./scanner/scan.js').ScanResult} result
44
+ */
45
+ export function renderFocusedMode(mode, result) {
46
+ switch (mode) {
47
+ case '--project-type':
48
+ console.log(`Project Type: ${result.projectType}`);
49
+ break;
50
+
51
+ case '--frameworks':
52
+ if (!result.projectType || result.projectType === 'Unknown') {
53
+ console.log('Frameworks: None detected');
54
+ } else {
55
+ console.log('Frameworks:');
56
+ console.log(`- ${result.projectType}`);
57
+ }
58
+ break;
59
+
60
+ case '--entry-points':
61
+ if (!result.entryPoints || result.entryPoints.length === 0) {
62
+ console.log('Entry Points: None detected');
63
+ } else {
64
+ console.log('Entry Points:');
65
+ result.entryPoints.forEach(ep => console.log(`- ${ep}`));
66
+ }
67
+ break;
68
+
69
+ case '--structure':
70
+ renderStructure(result);
71
+ break;
72
+
73
+ case '--configs':
74
+ console.log('Configuration Files');
75
+ console.log('───────────────────\n');
76
+ if (!result.configs || result.configs.length === 0) {
77
+ console.log('No configuration files detected.');
78
+ } else {
79
+ result.configs.forEach(c => console.log(`✓ ${c}`));
80
+ }
81
+ break;
82
+
83
+ case '--scripts':
84
+ console.log('Available Scripts\n');
85
+ if (!result.scripts || result.scripts.length === 0) {
86
+ console.log('No scripts found');
87
+ } else {
88
+ const maxNameLen = Math.max(...result.scripts.map(s => s.name.length));
89
+ result.scripts.forEach(s => {
90
+ const paddedName = s.name.padEnd(maxNameLen + 6, ' ');
91
+ console.log(`${paddedName}${s.command}`);
92
+ });
93
+ }
94
+ break;
95
+ }
96
+ }
@@ -99,8 +99,11 @@ function row(label, value, ...valueCodes) {
99
99
  * @param {string} prefix - Accumulated indentation
100
100
  * @param {boolean} isLast - Whether this is the last sibling
101
101
  * @param {{ count: number, maxReached: boolean }} counter - Shared mutable file counter
102
+ * @param {number} limit - Max files to render
103
+ * @param {number} depth - Current depth
104
+ * @param {number} maxDepth - Max recursion depth
102
105
  */
103
- function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
106
+ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth = 0, maxDepth = 4) {
104
107
  if (counter.maxReached) return;
105
108
  if (depth >= maxDepth) return;
106
109
 
@@ -117,7 +120,7 @@ function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
117
120
  }
118
121
 
119
122
  for (let i = 0; i < children.length; i++) {
120
- if (counter.count >= PREVIEW_LIMIT) {
123
+ if (counter.count >= limit) {
121
124
  console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
122
125
  counter.maxReached = true;
123
126
  break;
@@ -125,7 +128,7 @@ function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
125
128
  // Check if this child will be the last one we render due to limits
126
129
  let willBeLast = i === children.length - 1;
127
130
 
128
- renderTree(children[i], prefix + extension, willBeLast, counter, depth + 1, maxDepth);
131
+ renderTree(children[i], prefix + extension, willBeLast, counter, limit, depth + 1, maxDepth);
129
132
  if (counter.maxReached) break;
130
133
  }
131
134
  } else {
@@ -170,6 +173,8 @@ export function render(result, options = {}) {
170
173
  rootPath,
171
174
  projectType,
172
175
  entryPoints,
176
+ configs = [],
177
+ scripts = [],
173
178
  tree,
174
179
  flatFiles,
175
180
  totalFolders,
@@ -201,6 +206,28 @@ export function render(result, options = {}) {
201
206
  }
202
207
  }
203
208
 
209
+ // ── Configuration Files ───────────────────────────────────────────────────
210
+ section('⚙️', 'Configuration Files');
211
+ if (configs.length === 0) {
212
+ console.log(paint(' No configuration files detected', C.dim));
213
+ } else {
214
+ for (const c of configs) {
215
+ console.log(` ${paint(c, C.white)}`);
216
+ }
217
+ }
218
+
219
+ // ── Package Scripts ───────────────────────────────────────────────────────
220
+ section('📜', 'Package Scripts');
221
+ if (scripts.length === 0) {
222
+ console.log(paint(' No package scripts detected', C.dim));
223
+ } 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)}`);
228
+ }
229
+ }
230
+
204
231
  // ── Structure Preview ─────────────────────────────────────────────────────
205
232
  section('📁', `Folder Structure ${paint(`(first ${PREVIEW_LIMIT} files)`, C.dim)}`);
206
233
 
@@ -228,3 +255,28 @@ export function render(result, options = {}) {
228
255
  console.log(paint(' ✅ Scan complete.', C.green));
229
256
  console.log('');
230
257
  }
258
+
259
+ /**
260
+ * Render only the project structure for the --structure flag.
261
+ *
262
+ * @param {import('../scanner/scan.js').ScanResult} result
263
+ */
264
+ export function renderStructure(result) {
265
+ const { tree, flatFiles } = result;
266
+
267
+ console.log(paint('Project Structure:', C.bold, C.white));
268
+ console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
269
+
270
+ const counter = { count: 0, maxReached: false };
271
+ const children = tree.children ?? [];
272
+ const limit = flatFiles.length; // No preview limit for focused --structure
273
+
274
+ for (let i = 0; i < children.length; i++) {
275
+ if (counter.count >= limit) {
276
+ console.log(`└── ${paint('...', C.dim)}`);
277
+ break;
278
+ }
279
+ renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
280
+ if (counter.maxReached) break;
281
+ }
282
+ }
@@ -625,6 +625,7 @@ const icon = {
625
625
  tree: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
626
626
  bar: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>`,
627
627
  info: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
628
+ terminal: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
628
629
  };
629
630
 
630
631
  // ---------------------------------------------------------------------------
@@ -748,6 +749,82 @@ function renderEntryPoints(entryPoints) {
748
749
  </div>`;
749
750
  }
750
751
 
752
+ /**
753
+ * Render the configuration files section.
754
+ *
755
+ * @param {string[]} configs
756
+ * @returns {string}
757
+ */
758
+ function renderConfigurationFiles(configs) {
759
+ const count = configs.length;
760
+
761
+ const body = count === 0
762
+ ? `<p class="empty-msg">No configuration files detected.</p>`
763
+ : `<ul class="entry-list">
764
+ ${configs.map(c => `
765
+ <li class="entry-item">
766
+ <span class="entry-dot"></span>
767
+ ${esc(c)}
768
+ </li>`).join('')}
769
+ </ul>`;
770
+
771
+ const countBadge = count > 0
772
+ ? `<span class="section-count">${count} found</span>`
773
+ : '';
774
+
775
+ return `
776
+ <div class="section">
777
+ <div class="section-header">
778
+ <div class="section-icon">${icon.file()}</div>
779
+ <span class="section-title">Configuration Files</span>
780
+ ${countBadge}
781
+ </div>
782
+ <div class="section-body">${body}</div>
783
+ </div>`;
784
+ }
785
+
786
+ /**
787
+ * Render the package scripts section.
788
+ *
789
+ * @param {Array<{name: string, command: string}>} scripts
790
+ * @returns {string}
791
+ */
792
+ function renderPackageScripts(scripts) {
793
+ const count = scripts.length;
794
+
795
+ const body = count === 0
796
+ ? `<p class="empty-msg" style="padding: 1.5rem">No package scripts detected.</p>`
797
+ : `<table class="data-table">
798
+ <thead>
799
+ <tr>
800
+ <th>Script</th>
801
+ <th>Command</th>
802
+ </tr>
803
+ </thead>
804
+ <tbody>
805
+ ${scripts.map(s => `
806
+ <tr>
807
+ <td><code>${esc(s.name)}</code></td>
808
+ <td class="val-plain"><code>${esc(s.command)}</code></td>
809
+ </tr>`).join('')}
810
+ </tbody>
811
+ </table>`;
812
+
813
+ const countBadge = count > 0
814
+ ? `<span class="section-count">${count} found</span>`
815
+ : '';
816
+
817
+ return `
818
+ <div class="section">
819
+ <div class="section-header">
820
+ <div class="section-icon">${icon.terminal()}</div>
821
+ <span class="section-title">Package Scripts</span>
822
+ ${countBadge}
823
+ </div>
824
+ <div class="section-body" style="padding:0">${body}</div>
825
+ </div>`;
826
+ }
827
+
751
828
  /**
752
829
  * Render the folder structure section with a dark <pre><code> tree.
753
830
  *
@@ -873,7 +950,7 @@ function renderFooter() {
873
950
  * @param {{ cwd?: string }} [options]
874
951
  */
875
952
  export function render(result, options = {}) {
876
- const { rootPath, projectType, entryPoints, flatFiles } = result;
953
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles } = result;
877
954
 
878
955
  const cwd = options.cwd ?? process.cwd();
879
956
  const relRoot = path.relative(cwd, rootPath) || '.';
@@ -898,6 +975,8 @@ export function render(result, options = {}) {
898
975
  <main>
899
976
  ${renderSummaryCards(result)}
900
977
  ${renderEntryPoints(entryPoints)}
978
+ ${renderConfigurationFiles(configs)}
979
+ ${renderPackageScripts(scripts)}
901
980
  ${renderFolderStructure(flatFiles, rootName)}
902
981
  ${renderStats(result)}
903
982
  ${renderScanInfo()}
@@ -26,6 +26,8 @@ export function render(result, options = {}) {
26
26
  rootPath,
27
27
  projectType,
28
28
  entryPoints,
29
+ configs = [],
30
+ scripts = [],
29
31
  tree,
30
32
  flatFiles,
31
33
  totalFolders,
@@ -47,6 +49,8 @@ export function render(result, options = {}) {
47
49
  scanDurationMs: Math.round(scanDurationMs),
48
50
  },
49
51
  entryPoints,
52
+ configs,
53
+ scripts,
50
54
  structure: (tree.children || []).map(mapTree)
51
55
  };
52
56
 
@@ -218,6 +218,50 @@ function sectionSummary(out, result, relRoot) {
218
218
  out.push('---');
219
219
  }
220
220
 
221
+ /**
222
+ * @param {string[]} out
223
+ * @param {string[]} configs
224
+ */
225
+ function sectionConfigurationFiles(out, configs) {
226
+ out.push('');
227
+ out.push('## Configuration Files');
228
+ out.push('');
229
+
230
+ if (configs.length === 0) {
231
+ out.push('No configuration files detected.');
232
+ } else {
233
+ for (const c of configs) {
234
+ out.push(`- ${c}`);
235
+ }
236
+ }
237
+
238
+ out.push('');
239
+ out.push('---');
240
+ }
241
+
242
+ /**
243
+ * @param {string[]} out
244
+ * @param {Array<{name: string, command: string}>} scripts
245
+ */
246
+ function sectionPackageScripts(out, scripts) {
247
+ out.push('');
248
+ out.push('## Package Scripts');
249
+ out.push('');
250
+
251
+ if (scripts.length === 0) {
252
+ out.push('No package scripts detected.');
253
+ } else {
254
+ out.push('| Script | Command |');
255
+ out.push('|--------|---------|');
256
+ for (const s of scripts) {
257
+ out.push(`| \`${s.name}\` | \`${s.command}\` |`);
258
+ }
259
+ }
260
+
261
+ out.push('');
262
+ out.push('---');
263
+ }
264
+
221
265
  /**
222
266
  * @param {string[]} out
223
267
  * @param {string[]} entryPoints
@@ -308,7 +352,7 @@ function sectionScanInfo(out) {
308
352
  * @param {{ cwd?: string }} [options]
309
353
  */
310
354
  export function render(result, options = {}) {
311
- const { rootPath, entryPoints, flatFiles } = result;
355
+ const { rootPath, entryPoints, configs = [], scripts = [], flatFiles } = result;
312
356
 
313
357
  const cwd = options.cwd ?? process.cwd();
314
358
  const relRoot = path.relative(cwd, rootPath) || '.';
@@ -320,6 +364,8 @@ export function render(result, options = {}) {
320
364
  sectionTitle(out);
321
365
  sectionSummary(out, result, relRoot);
322
366
  sectionEntryPoints(out, entryPoints);
367
+ sectionConfigurationFiles(out, configs);
368
+ sectionPackageScripts(out, scripts);
323
369
  sectionFolderStructure(out, flatFiles, rootName);
324
370
  sectionStatistics(out, result);
325
371
  sectionScanInfo(out);
@@ -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
@@ -453,6 +457,10 @@ export function scan(targetPath, options = {}) {
453
457
  const flatFiles = [];
454
458
  /** @type {Array<string>} */
455
459
  let entryPoints = [];
460
+ /** @type {Array<string>} */
461
+ let configs = [];
462
+ /** @type {Array<{name: string, command: string}>} */
463
+ let scripts = [];
456
464
 
457
465
  const startTime = performance.now();
458
466
  let tree;
@@ -465,6 +473,8 @@ export function scan(targetPath, options = {}) {
465
473
  // Count all directory nodes in the tree (excluding root itself).
466
474
  totalFolders = countFolders(tree) - 1;
467
475
  entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
476
+ configs = detectConfigs(flatFiles).configs;
477
+ scripts = detectScripts(rootPath).scripts;
468
478
  } else if (stat.isFile()) {
469
479
  const relFilePath = toPosix(path.basename(rootPath));
470
480
  tree = {
@@ -481,6 +491,8 @@ export function scan(targetPath, options = {}) {
481
491
  };
482
492
  flatFiles.push(relFilePath);
483
493
  entryPoints = [relFilePath]; // A single file is its own entry point
494
+ configs = detectConfigs(flatFiles).configs;
495
+ scripts = detectScripts(path.dirname(rootPath)).scripts;
484
496
  } else {
485
497
  throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
486
498
  }
@@ -491,6 +503,8 @@ export function scan(targetPath, options = {}) {
491
503
  rootPath,
492
504
  projectType,
493
505
  entryPoints,
506
+ configs,
507
+ scripts,
494
508
  tree,
495
509
  flatFiles,
496
510
  totalFolders,