@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 +51 -7
- package/bin/toren.js +35 -10
- package/package.json +2 -1
- package/src/detectors/config-detector.js +81 -0
- package/src/detectors/script-detector.js +32 -0
- package/src/focused-output.js +96 -0
- package/src/renderers/console-renderer.js +55 -3
- package/src/renderers/html-renderer.js +80 -1
- package/src/renderers/json-renderer.js +4 -0
- package/src/renderers/markdown-renderer.js +47 -1
- package/src/scanner/scan.js +14 -0
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.
|
|
139
|
+
Toren v1.0.4 — Codebase Onboarding Intelligence
|
|
96
140
|
|
|
97
|
-
|
|
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
|
-
|
|
149
|
+
Entry Points
|
|
106
150
|
────────────────────────────────────────────────────────────────────────────────
|
|
107
151
|
→ src/main.tsx
|
|
108
152
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
256
|
+
## License
|
|
213
257
|
|
|
214
258
|
Distributed under the MIT License. See `LICENSE` for more information.
|
|
215
259
|
|
|
216
260
|
---
|
|
217
261
|
|
|
218
|
-
##
|
|
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]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
+
"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 >=
|
|
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);
|
package/src/scanner/scan.js
CHANGED
|
@@ -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,
|