@lakindu_perera/toren 1.0.2 → 1.0.4
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 +172 -89
- package/bin/toren.js +109 -30
- package/package.json +5 -6
- package/src/focused-output.js +71 -0
- package/src/renderers/console-renderer.js +31 -3
- package/src/scanner/scan.js +74 -23
- package/src/renderers/tree-renderer.js +0 -67
package/README.md
CHANGED
|
@@ -1,164 +1,247 @@
|
|
|
1
|
-
# Toren — Codebase
|
|
1
|
+
# Toren — Codebase Analyzer CLI
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The fastest way to understand any project structure. A zero-dependency codebase scanner CLI for modern developers.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lakindu_perera/toren)
|
|
6
|
-
[](https://nodejs.org)
|
|
6
|
+
[](https://nodejs.org)
|
|
8
7
|
[](package.json)
|
|
8
|
+
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## What is Toren?
|
|
13
13
|
|
|
14
|
-
**Toren** is a
|
|
14
|
+
**Toren** is a fast, lightweight **codebase analyzer CLI** and **project scanner tool** designed to generate instant onboarding intelligence reports. By recursively scanning any local directory, Toren detects the underlying project framework, identifies critical entry points, and visualizes the folder structure—all in milliseconds.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Built with zero external dependencies, this **Node.js repository explorer** is the ultimate **developer onboarding tool** to help you conquer the "first 5 minutes" of navigating an unfamiliar repository.
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **Zero Dependencies**: A pure Node.js CLI tool with zero external runtime packages.
|
|
30
|
-
- **Machine-Readable Output**: Full JSON support for seamless integration with other developer tools and AI agents.
|
|
22
|
+
- **Recursive Project Scanning**: Fast directory traversal using native Node.js APIs.
|
|
23
|
+
- **Framework Detection**: Instantly identifies Node.js, React, Next.js, Vue, Nuxt, Angular, Svelte, Python, Go, Rust, Spring Boot, Ruby, PHP, Elixir and more.
|
|
24
|
+
- **Entry Point Detection**: Automatically pinpoints where execution begins (e.g., `index.js`, `main.ts`, `App.tsx`, `main.go`).
|
|
25
|
+
- **Project Structure Visualizer**: Generates clean, hierarchical file trees.
|
|
26
|
+
- **Multiple Output Formats**: Choose between `console` (default), `json`, `markdown`, or `html` reports.
|
|
27
|
+
- **Zero Dependencies**: A pure Node.js CLI tool with no external runtime packages. Lightning fast install, infinitely secure.
|
|
28
|
+
- **CLI Lifecycle Tools**: Native diagnostic and uninstallation tools (`--doctor`, `--uninstall`).
|
|
31
29
|
|
|
32
30
|
---
|
|
33
31
|
|
|
34
32
|
## Installation
|
|
35
33
|
|
|
36
|
-
Install
|
|
34
|
+
Install Toren globally via npm to make the **repository inspection CLI** available anywhere on your machine:
|
|
37
35
|
|
|
38
36
|
```bash
|
|
39
37
|
npm install -g @lakindu_perera/toren
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
Alternatively, you can run it instantly without global installation:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
npx @lakindu_perera/toren
|
|
46
|
-
```
|
|
47
|
-
|
|
48
40
|
**Requirements:** Node.js 18.0.0 or higher.
|
|
49
41
|
|
|
50
42
|
---
|
|
51
43
|
|
|
52
|
-
## Usage
|
|
53
|
-
|
|
54
|
-
Navigate to your target project folder or point Toren directly to a repository.
|
|
55
|
-
|
|
56
|
-
Scan the current directory for an instant summary:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
toren .
|
|
60
|
-
```
|
|
44
|
+
## CLI Usage
|
|
61
45
|
|
|
62
|
-
|
|
46
|
+
Toren is designed to be simple and intuitive. Point it at any directory to generate an immediate intelligence report.
|
|
63
47
|
|
|
64
48
|
```bash
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
# Scan a specific path
|
|
50
|
+
toren /path/to/project
|
|
67
51
|
|
|
68
|
-
|
|
52
|
+
# Scan the current directory
|
|
53
|
+
toren .
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
toren
|
|
55
|
+
# Export results in different formats
|
|
56
|
+
toren --format json
|
|
57
|
+
toren --format markdown
|
|
58
|
+
toren --format html
|
|
59
|
+
|
|
60
|
+
# Save output to a file
|
|
61
|
+
toren --format markdown > PROJECT_REPORT.md
|
|
62
|
+
toren --format html > report.html
|
|
63
|
+
toren --format json > scan.json
|
|
64
|
+
|
|
65
|
+
# Focused Output Modes (Mutually Exclusive)
|
|
66
|
+
toren --project-type
|
|
67
|
+
toren --frameworks
|
|
68
|
+
toren --entry-points
|
|
69
|
+
toren --structure
|
|
70
|
+
|
|
71
|
+
# Lifecycle & Help Commands
|
|
72
|
+
toren --help
|
|
73
|
+
toren --version
|
|
74
|
+
toren --doctor
|
|
75
|
+
toren --uninstall
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
### All Flags
|
|
79
|
+
|
|
80
|
+
| Flag | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `[path]` | Directory to scan. Defaults to the current directory (`.`). |
|
|
83
|
+
| `--project-type` | Show detected project type only. |
|
|
84
|
+
| `--frameworks` | Show detected frameworks only. |
|
|
85
|
+
| `--entry-points` | Show detected entry points only. |
|
|
86
|
+
| `--structure` | Show repository structure only. |
|
|
87
|
+
| `--format <type>` | Output format: `console` (default), `json`, `markdown`, `html`. |
|
|
88
|
+
| `--include-hidden` | Include hidden files and dot-directories in the scan. |
|
|
89
|
+
| `--max-files <N>` | Override the default 50,000-file scan limit. |
|
|
90
|
+
| `--help` / `-h` | Show usage information. |
|
|
91
|
+
| `--version` / `-V` | Print the installed version number. |
|
|
92
|
+
| `--doctor` | Diagnose the global installation health. |
|
|
93
|
+
| `--uninstall` | Safely remove Toren from the global npm environment. |
|
|
94
|
+
|
|
95
|
+
> **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
|
|
96
|
+
> **Note:** Focused output flags (`--project-type`, `--frameworks`, `--entry-points`, `--structure`) are mutually exclusive.
|
|
75
97
|
|
|
76
98
|
---
|
|
77
99
|
|
|
78
|
-
##
|
|
100
|
+
## Output Examples
|
|
79
101
|
|
|
80
|
-
|
|
102
|
+
### Focused Output Example
|
|
103
|
+
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:
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
- **Spring Boot** (Java)
|
|
86
|
-
- **Python** (including Pipenv and pyproject.toml setups)
|
|
87
|
-
- **Go**
|
|
88
|
-
- **Rust**
|
|
89
|
-
- **Vue.js**
|
|
90
|
-
- **Angular**
|
|
91
|
-
- **Svelte**
|
|
92
|
-
- **PHP**
|
|
93
|
-
- **Ruby**
|
|
94
|
-
- **Elixir**
|
|
105
|
+
```bash
|
|
106
|
+
$ toren --project-type
|
|
107
|
+
Project Type: Node.js / JavaScript
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
$ toren --frameworks
|
|
110
|
+
Frameworks:
|
|
111
|
+
- React
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
$ toren --entry-points
|
|
114
|
+
Entry Points:
|
|
115
|
+
- src/main.tsx
|
|
116
|
+
```
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
### Console Output Example
|
|
119
|
+
The default `console` format renders a beautiful summary directly in your terminal:
|
|
101
120
|
|
|
102
|
-
```
|
|
103
|
-
Toren v1.0.
|
|
121
|
+
```text
|
|
122
|
+
Toren v1.0.4 — Codebase Onboarding Intelligence
|
|
104
123
|
|
|
105
124
|
🔍 Project Summary
|
|
106
125
|
────────────────────────────────────────────────────────────────────────────────
|
|
107
|
-
Path: ./my-app
|
|
108
|
-
Project type:
|
|
109
|
-
Total files:
|
|
110
|
-
Total folders:
|
|
111
|
-
Scan duration:
|
|
126
|
+
Path: ./my-react-app
|
|
127
|
+
Project type: React
|
|
128
|
+
Total files: 32
|
|
129
|
+
Total folders: 6
|
|
130
|
+
Scan duration: 4 ms
|
|
112
131
|
|
|
113
132
|
🚪 Entry Points
|
|
114
133
|
────────────────────────────────────────────────────────────────────────────────
|
|
115
|
-
→ src/
|
|
116
|
-
→ src/app/layout.tsx
|
|
134
|
+
→ src/main.tsx
|
|
117
135
|
|
|
118
136
|
📁 Folder Structure (first 20 files)
|
|
119
137
|
────────────────────────────────────────────────────────────────────────────────
|
|
120
|
-
my-app/
|
|
138
|
+
my-react-app/
|
|
121
139
|
├── public/
|
|
122
|
-
│ └──
|
|
140
|
+
│ └── vite.svg
|
|
123
141
|
├── src/
|
|
124
|
-
│ ├──
|
|
125
|
-
│ │
|
|
126
|
-
│
|
|
127
|
-
│ ├──
|
|
128
|
-
│
|
|
129
|
-
│
|
|
130
|
-
|
|
131
|
-
│ └── utils.ts
|
|
132
|
-
├── .eslintrc.json
|
|
133
|
-
├── next.config.js
|
|
142
|
+
│ ├── assets/
|
|
143
|
+
│ │ └── react.svg
|
|
144
|
+
│ ├── App.css
|
|
145
|
+
│ ├── App.tsx
|
|
146
|
+
│ ├── index.css
|
|
147
|
+
│ └── main.tsx
|
|
148
|
+
├── index.html
|
|
134
149
|
├── package.json
|
|
135
|
-
└──
|
|
136
|
-
… and
|
|
150
|
+
└── vite.config.ts
|
|
151
|
+
… and 21 more file(s) not shown
|
|
137
152
|
|
|
138
153
|
────────────────────────────────────────────────────────────────────────────────
|
|
139
154
|
✅ Scan complete.
|
|
140
155
|
```
|
|
141
156
|
|
|
157
|
+
### JSON Output Example
|
|
158
|
+
Generate machine-readable output for scripts, toolchains, or AI context windows by running `toren --format json`:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"project": {
|
|
163
|
+
"path": "./my-react-app",
|
|
164
|
+
"type": "React",
|
|
165
|
+
"framework": "React"
|
|
166
|
+
},
|
|
167
|
+
"summary": {
|
|
168
|
+
"totalFiles": 32,
|
|
169
|
+
"totalFolders": 6,
|
|
170
|
+
"scanDurationMs": 4
|
|
171
|
+
},
|
|
172
|
+
"entryPoints": [
|
|
173
|
+
"src/main.tsx"
|
|
174
|
+
],
|
|
175
|
+
"structure": [
|
|
176
|
+
{
|
|
177
|
+
"type": "folder",
|
|
178
|
+
"name": "src",
|
|
179
|
+
"children": [
|
|
180
|
+
{ "type": "file", "name": "App.tsx" },
|
|
181
|
+
{ "type": "file", "name": "main.tsx" }
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{ "type": "file", "name": "package.json" }
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
*Note: You can also generate rich, GitHub-flavored Markdown (`--format markdown`) or self-contained HTML reports (`--format html`) for documentation purposes!*
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Architecture
|
|
194
|
+
|
|
195
|
+
Toren's internal architecture emphasizes modular design, separation of concerns, and a strict **zero dependency** philosophy.
|
|
196
|
+
|
|
197
|
+
- **`bin/toren.js`** — The CLI entry point. Handles argument parsing, option validation, and orchestrates the scanning and rendering phases.
|
|
198
|
+
- **`src/scanner/scan.js`** — The core scanning engine. Safely traverses the file system, executes framework detection heuristics, and extracts entry points.
|
|
199
|
+
- **`src/renderers/`** — The output formatting system. A highly decoupled registry of formatters (`console`, `json`, `html`, `markdown`). Each renderer consumes the raw scan data and formats it independently.
|
|
200
|
+
- **`src/lifecycle.js`** — Dedicated install/uninstall diagnostic tools (`--doctor`, `--uninstall`) to ensure the global CLI binary remains healthy.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Why Toren Exists
|
|
205
|
+
|
|
206
|
+
Modern software development moves fast, but **onboarding into large codebases is slow**.
|
|
207
|
+
|
|
208
|
+
When developers join a new team, review a complex pull request, or audit an open-source project, they waste valuable time manually clicking through folders and reading `package.json` files just to understand the basic structure.
|
|
209
|
+
|
|
210
|
+
**Toren solves the "first 5 minutes of any repo" problem.**
|
|
211
|
+
|
|
212
|
+
As a dedicated **developer onboarding tool**, Toren automates the initial discovery phase. In a single command, it tells you exactly what the project is, where the code starts executing, and how the folders are structured. By eliminating the manual guesswork of repository inspection, Toren drastically improves developer productivity.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Roadmap (Future Improvements)
|
|
217
|
+
|
|
218
|
+
- `.torenignore` configuration file support
|
|
219
|
+
- Dependency graph analysis and visualization
|
|
220
|
+
- Plugin system for custom renderer injection
|
|
221
|
+
- AI-generated natural language project summaries
|
|
222
|
+
|
|
142
223
|
---
|
|
143
224
|
|
|
144
225
|
## Contributing
|
|
145
226
|
|
|
146
|
-
Contributions to improve this
|
|
227
|
+
Contributions to improve this codebase analyzer CLI are always welcome!
|
|
147
228
|
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
151
|
-
4.
|
|
152
|
-
5.
|
|
229
|
+
1. Fork the repository
|
|
230
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
231
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
232
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
233
|
+
5. Open a Pull Request
|
|
153
234
|
|
|
154
|
-
Please ensure
|
|
235
|
+
*Please ensure all new features maintain the strict zero-dependency architecture rule.*
|
|
155
236
|
|
|
156
237
|
---
|
|
157
238
|
|
|
158
|
-
## License
|
|
239
|
+
## 📄 License
|
|
159
240
|
|
|
160
|
-
|
|
241
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
161
242
|
|
|
162
243
|
---
|
|
163
244
|
|
|
164
|
-
|
|
245
|
+
## 👨💻 Author
|
|
246
|
+
|
|
247
|
+
Built with by **[Lakindu Perera](https://github.com/lakindudev)**.
|
package/bin/toren.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* toren --help Print usage
|
|
11
11
|
* toren --doctor Check global installation health
|
|
12
12
|
* toren --uninstall Safely remove global installation
|
|
13
|
+
* toren --include-hidden Include hidden files (dot-files) in scan
|
|
14
|
+
* toren --max-files <N> Override the max file scan limit
|
|
13
15
|
*
|
|
14
16
|
* Adding a new output format
|
|
15
17
|
* ──────────────────────────
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
import { createRequire } from 'node:module';
|
|
24
26
|
import { scan } from '../src/scanner/scan.js';
|
|
25
27
|
import renderers from '../src/renderers/index.js';
|
|
28
|
+
import { getFocusedModeInfo, renderFocusedMode, FOCUSED_FLAGS } from '../src/focused-output.js';
|
|
26
29
|
import { runDoctor, runUninstall } from '../src/lifecycle.js';
|
|
27
30
|
|
|
28
31
|
const require = createRequire(import.meta.url);
|
|
@@ -42,12 +45,20 @@ function printHelp() {
|
|
|
42
45
|
const formatList = SUPPORTED_FORMATS.map(f => ` ${f}`).join('\n');
|
|
43
46
|
console.log(`
|
|
44
47
|
\x1b[1mUsage:\x1b[0m
|
|
45
|
-
toren [path]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
--format <type> Output as console, json, markdown, or html
|
|
56
|
+
--include-hidden Include hidden files and folders
|
|
57
|
+
--max-files <n> Set scan file limit
|
|
58
|
+
--help Show this help message
|
|
59
|
+
--version Show version number
|
|
60
|
+
--doctor Run CLI diagnostics
|
|
61
|
+
--uninstall Remove Toren global installation
|
|
51
62
|
|
|
52
63
|
\x1b[1mOutput Formats:\x1b[0m
|
|
53
64
|
${formatList}
|
|
@@ -55,10 +66,13 @@ ${formatList}
|
|
|
55
66
|
console is the default.
|
|
56
67
|
|
|
57
68
|
\x1b[1mExamples:\x1b[0m
|
|
58
|
-
toren .
|
|
59
|
-
toren ./my-project
|
|
60
|
-
toren --format json
|
|
61
|
-
toren --format
|
|
69
|
+
toren . Scan the current directory
|
|
70
|
+
toren ./my-project Scan a specific project folder
|
|
71
|
+
toren --format json . Output results as JSON
|
|
72
|
+
toren --format markdown . > out.md Save a Markdown report to a file
|
|
73
|
+
toren --format html . > out.html Save an HTML report to a file
|
|
74
|
+
toren --include-hidden . Include hidden dot-files in scan
|
|
75
|
+
toren --max-files 100000 . Override the 50k file scan limit
|
|
62
76
|
`);
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -69,9 +83,10 @@ ${formatList}
|
|
|
69
83
|
/**
|
|
70
84
|
* @typedef {Object} ParsedArgs
|
|
71
85
|
* @property {'exit'|'wait'|'scan'} action
|
|
72
|
-
* @property {string} [target]
|
|
73
|
-
* @property {string} [format]
|
|
86
|
+
* @property {string} [target] - Resolved path to scan
|
|
87
|
+
* @property {string} [format] - Renderer format name
|
|
74
88
|
* @property {boolean} [includeHidden] - Whether to include hidden files
|
|
89
|
+
* @property {number} [maxFiles] - Max files to scan
|
|
75
90
|
*/
|
|
76
91
|
|
|
77
92
|
/**
|
|
@@ -89,18 +104,18 @@ function parseArgs() {
|
|
|
89
104
|
// ── Informational flags ─────────────────────────────────────────────────
|
|
90
105
|
if (args.includes('--help') || args.includes('-h')) {
|
|
91
106
|
printHelp();
|
|
92
|
-
return { action: 'exit' };
|
|
107
|
+
return { action: 'exit', code: 0 };
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
if (args.includes('--version') || args.includes('-
|
|
110
|
+
if (args.includes('--version') || args.includes('-V') || args.includes('-v')) {
|
|
96
111
|
console.log(pkg.version);
|
|
97
|
-
return { action: 'exit' };
|
|
112
|
+
return { action: 'exit', code: 0 };
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
// ── Lifecycle commands ──────────────────────────────────────────────────
|
|
101
116
|
if (args.includes('--doctor')) {
|
|
102
117
|
runDoctor(pkg.version);
|
|
103
|
-
return { action: 'exit' };
|
|
118
|
+
return { action: 'exit', code: 0 };
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
if (args.includes('--uninstall')) {
|
|
@@ -115,40 +130,93 @@ function parseArgs() {
|
|
|
115
130
|
|
|
116
131
|
const formatIdx = args.indexOf('--format');
|
|
117
132
|
if (formatIdx !== -1) {
|
|
118
|
-
|
|
119
|
-
// If the
|
|
120
|
-
|
|
133
|
+
const nextToken = args[formatIdx + 1];
|
|
134
|
+
// If the next token is missing or starts with '-', the user omitted the value.
|
|
135
|
+
if (nextToken === undefined || nextToken.startsWith('-')) {
|
|
136
|
+
console.error('');
|
|
137
|
+
console.error('\x1b[31m --format requires a value.\x1b[0m');
|
|
138
|
+
console.error('');
|
|
139
|
+
console.error(` Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
|
|
140
|
+
console.error('');
|
|
141
|
+
console.error(' Run \x1b[36mtoren --help\x1b[0m for usage.');
|
|
142
|
+
console.error('');
|
|
143
|
+
return { action: 'exit', code: 1 };
|
|
144
|
+
}
|
|
145
|
+
format = nextToken;
|
|
121
146
|
} else if (args.includes('--json')) {
|
|
122
147
|
format = 'json';
|
|
123
148
|
}
|
|
124
149
|
|
|
150
|
+
// ── Focused Output Flags ────────────────────────────────────────────────
|
|
151
|
+
const focusedInfo = getFocusedModeInfo(args);
|
|
152
|
+
|
|
153
|
+
if (focusedInfo.error) {
|
|
154
|
+
console.error(focusedInfo.message);
|
|
155
|
+
return { action: 'exit', code: 1 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const focusedMode = focusedInfo.mode;
|
|
159
|
+
|
|
125
160
|
// ── Hidden files ────────────────────────────────────────────────────────
|
|
126
161
|
const includeHidden = args.includes('--include-hidden');
|
|
127
162
|
|
|
128
|
-
|
|
129
163
|
// ── Target path ─────────────────────────────────────────────────────────
|
|
130
164
|
// Build the set of tokens that are consumed as values by named flags so
|
|
131
165
|
// we don't accidentally treat them as the positional path argument.
|
|
132
|
-
// Currently only --format consumes a value token.
|
|
133
166
|
const consumedValues = new Set();
|
|
134
|
-
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined) {
|
|
167
|
+
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined && !args[formatIdx + 1].startsWith('-')) {
|
|
135
168
|
consumedValues.add(args[formatIdx + 1]);
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
// ── Max files ───────────────────────────────────────────────────────────
|
|
172
|
+
let maxFiles = undefined;
|
|
173
|
+
const maxFilesIdx = args.indexOf('--max-files');
|
|
174
|
+
if (maxFilesIdx !== -1) {
|
|
175
|
+
const rawVal = args[maxFilesIdx + 1];
|
|
176
|
+
if (rawVal === undefined || rawVal.startsWith('-')) {
|
|
177
|
+
console.error('');
|
|
178
|
+
console.error('\x1b[31m --max-files requires a numeric value.\x1b[0m');
|
|
179
|
+
console.error(' Example: toren --max-files 100000');
|
|
180
|
+
console.error('');
|
|
181
|
+
return { action: 'exit', code: 1 };
|
|
182
|
+
}
|
|
183
|
+
maxFiles = parseInt(rawVal, 10);
|
|
184
|
+
if (isNaN(maxFiles) || maxFiles < 1) {
|
|
185
|
+
console.error('');
|
|
186
|
+
console.error(`\x1b[31m --max-files must be a positive integer, got: ${rawVal}\x1b[0m`);
|
|
187
|
+
console.error('');
|
|
188
|
+
return { action: 'exit', code: 1 };
|
|
189
|
+
}
|
|
190
|
+
consumedValues.add(rawVal);
|
|
191
|
+
}
|
|
192
|
+
|
|
138
193
|
// First non-flag, non-consumed token is the target path; default to cwd.
|
|
139
194
|
const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
|
|
140
195
|
|
|
141
196
|
// ── Unknown flag check ──────────────────────────────────────────────────
|
|
142
|
-
const knownFlags = new Set([
|
|
143
|
-
|
|
144
|
-
|
|
197
|
+
const knownFlags = new Set([
|
|
198
|
+
'--help', '-h',
|
|
199
|
+
'--version', '-V', '-v',
|
|
200
|
+
'--doctor',
|
|
201
|
+
'--uninstall',
|
|
202
|
+
'--format',
|
|
203
|
+
'--json',
|
|
204
|
+
...FOCUSED_FLAGS,
|
|
205
|
+
'--include-hidden',
|
|
206
|
+
'--max-files',
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
const unknownFlag = args.find(
|
|
210
|
+
a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a),
|
|
211
|
+
);
|
|
212
|
+
|
|
145
213
|
if (unknownFlag) {
|
|
146
214
|
console.error(`\x1b[31m Unknown flag: ${unknownFlag}\x1b[0m`);
|
|
147
215
|
console.error(` Run \x1b[36mtoren --help\x1b[0m for usage.\n`);
|
|
148
|
-
return { action: 'exit' };
|
|
216
|
+
return { action: 'exit', code: 1 };
|
|
149
217
|
}
|
|
150
218
|
|
|
151
|
-
return { action: 'scan', target, format, includeHidden };
|
|
219
|
+
return { action: 'scan', target, format, includeHidden, maxFiles, focusedMode };
|
|
152
220
|
}
|
|
153
221
|
|
|
154
222
|
// ---------------------------------------------------------------------------
|
|
@@ -183,7 +251,10 @@ function assertValidFormat(format) {
|
|
|
183
251
|
|
|
184
252
|
(function main() {
|
|
185
253
|
const parsed = parseArgs();
|
|
186
|
-
|
|
254
|
+
|
|
255
|
+
if (parsed.action === 'exit') {
|
|
256
|
+
process.exit(parsed.code ?? 0);
|
|
257
|
+
}
|
|
187
258
|
if (parsed.action === 'wait') return;
|
|
188
259
|
|
|
189
260
|
// Validate before scanning — fail fast on bad format names.
|
|
@@ -192,8 +263,16 @@ function assertValidFormat(format) {
|
|
|
192
263
|
const render = renderers[parsed.format];
|
|
193
264
|
|
|
194
265
|
try {
|
|
195
|
-
const result = scan(parsed.target, {
|
|
196
|
-
|
|
266
|
+
const result = scan(parsed.target, {
|
|
267
|
+
includeHidden: parsed.includeHidden,
|
|
268
|
+
maxFiles: parsed.maxFiles,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (parsed.focusedMode) {
|
|
272
|
+
renderFocusedMode(parsed.focusedMode, result);
|
|
273
|
+
} else {
|
|
274
|
+
render(result, { cwd: process.cwd() });
|
|
275
|
+
}
|
|
197
276
|
} catch (err) {
|
|
198
277
|
// Render errors in the requested format where possible.
|
|
199
278
|
if (parsed.format === 'json') {
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakindu_perera/toren",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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": {
|
|
7
7
|
"toren": "bin/toren.js"
|
|
8
8
|
},
|
|
9
|
-
"preferGlobal": true,
|
|
10
|
-
"main": "bin/toren.js",
|
|
11
9
|
"files": [
|
|
12
10
|
"bin",
|
|
13
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node bin/toren.js",
|
|
17
|
-
"lint": "node --check bin/toren.js src/scanner/scan.js"
|
|
17
|
+
"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
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"cli tool",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"monorepo analyzer",
|
|
26
26
|
"node cli tool",
|
|
27
27
|
"developer tool",
|
|
28
|
-
"typescript cli",
|
|
29
28
|
"onboarding"
|
|
30
29
|
],
|
|
31
30
|
"author": "Lakindu Perera",
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates focused flags in the provided arguments.
|
|
16
|
+
*
|
|
17
|
+
* @param {string[]} args Process arguments
|
|
18
|
+
* @returns {{ error: boolean, message?: string, mode?: string|null }}
|
|
19
|
+
*/
|
|
20
|
+
export function getFocusedModeInfo(args) {
|
|
21
|
+
const activeFlags = FOCUSED_FLAGS.filter(flag => args.includes(flag));
|
|
22
|
+
|
|
23
|
+
if (activeFlags.length > 1) {
|
|
24
|
+
return {
|
|
25
|
+
error: true,
|
|
26
|
+
message: '\x1b[31mError: focused output flags are mutually exclusive. Please use only one of:\x1b[0m\n' +
|
|
27
|
+
FOCUSED_FLAGS.map(f => ` ${f}`).join('\n') + '\n'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
error: false,
|
|
33
|
+
mode: activeFlags[0] || null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renders the scan result based on the active focused mode.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} mode Active focused mode flag
|
|
41
|
+
* @param {import('./scanner/scan.js').ScanResult} result
|
|
42
|
+
*/
|
|
43
|
+
export function renderFocusedMode(mode, result) {
|
|
44
|
+
switch (mode) {
|
|
45
|
+
case '--project-type':
|
|
46
|
+
console.log(`Project Type: ${result.projectType}`);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case '--frameworks':
|
|
50
|
+
if (!result.projectType || result.projectType === 'Unknown') {
|
|
51
|
+
console.log('Frameworks: None detected');
|
|
52
|
+
} else {
|
|
53
|
+
console.log('Frameworks:');
|
|
54
|
+
console.log(`- ${result.projectType}`);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case '--entry-points':
|
|
59
|
+
if (!result.entryPoints || result.entryPoints.length === 0) {
|
|
60
|
+
console.log('Entry Points: None detected');
|
|
61
|
+
} else {
|
|
62
|
+
console.log('Entry Points:');
|
|
63
|
+
result.entryPoints.forEach(ep => console.log(`- ${ep}`));
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case '--structure':
|
|
68
|
+
renderStructure(result);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -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 {
|
|
@@ -228,3 +231,28 @@ export function render(result, options = {}) {
|
|
|
228
231
|
console.log(paint(' ✅ Scan complete.', C.green));
|
|
229
232
|
console.log('');
|
|
230
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Render only the project structure for the --structure flag.
|
|
237
|
+
*
|
|
238
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
239
|
+
*/
|
|
240
|
+
export function renderStructure(result) {
|
|
241
|
+
const { tree, flatFiles } = result;
|
|
242
|
+
|
|
243
|
+
console.log(paint('Project Structure:', C.bold, C.white));
|
|
244
|
+
console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
|
|
245
|
+
|
|
246
|
+
const counter = { count: 0, maxReached: false };
|
|
247
|
+
const children = tree.children ?? [];
|
|
248
|
+
const limit = flatFiles.length; // No preview limit for focused --structure
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < children.length; i++) {
|
|
251
|
+
if (counter.count >= limit) {
|
|
252
|
+
console.log(`└── ${paint('...', C.dim)}`);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
|
|
256
|
+
if (counter.maxReached) break;
|
|
257
|
+
}
|
|
258
|
+
}
|
package/src/scanner/scan.js
CHANGED
|
@@ -45,6 +45,10 @@ const IGNORED_DIRS = new Set([
|
|
|
45
45
|
'venv',
|
|
46
46
|
'.idea',
|
|
47
47
|
'.vscode',
|
|
48
|
+
'.svelte-kit',
|
|
49
|
+
'.turbo',
|
|
50
|
+
'.parcel-cache',
|
|
51
|
+
'.DS_Store'
|
|
48
52
|
]);
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -130,9 +134,10 @@ function shouldIgnore(name, dirent, includeHidden) {
|
|
|
130
134
|
* @param {string} rootPath - Absolute path of the scan root (for relative paths)
|
|
131
135
|
* @param {Array<string>} flatFiles - Accumulator for all relative file paths
|
|
132
136
|
* @param {boolean} includeHidden - Whether to include hidden files
|
|
137
|
+
* @param {number} maxFiles - Maximum number of files to scan before aborting
|
|
133
138
|
* @returns {DirNode}
|
|
134
139
|
*/
|
|
135
|
-
function walkDirectory(dirPath, rootPath, flatFiles, includeHidden) {
|
|
140
|
+
function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
|
|
136
141
|
const name = path.basename(dirPath);
|
|
137
142
|
let rawRelPath = path.relative(rootPath, dirPath) || '.';
|
|
138
143
|
const relPath = toPosix(rawRelPath);
|
|
@@ -149,9 +154,10 @@ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden) {
|
|
|
149
154
|
let entries;
|
|
150
155
|
try {
|
|
151
156
|
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
152
|
-
} catch
|
|
153
|
-
// Permission-denied or unreadable directory — skip
|
|
154
|
-
|
|
157
|
+
} catch {
|
|
158
|
+
// Permission-denied or unreadable directory — skip silently.
|
|
159
|
+
// Write to stderr so stdout remains clean for piped/redirected output.
|
|
160
|
+
process.stderr.write(`[toren] warning: skipping unreadable directory: ${dirPath}\n`);
|
|
155
161
|
return node;
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -168,10 +174,16 @@ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden) {
|
|
|
168
174
|
|
|
169
175
|
const childPath = path.join(dirPath, dirent.name);
|
|
170
176
|
|
|
177
|
+
// Explicitly skip symbolic links to prevent infinite loops and unsafe traversals
|
|
178
|
+
if (dirent.isSymbolicLink()) continue;
|
|
179
|
+
|
|
171
180
|
if (dirent.isDirectory()) {
|
|
172
|
-
const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden);
|
|
181
|
+
const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden, maxFiles);
|
|
173
182
|
node.children.push(childNode);
|
|
174
183
|
} else if (dirent.isFile()) {
|
|
184
|
+
if (flatFiles.length >= maxFiles) {
|
|
185
|
+
throw new Error(`Max file scan limit exceeded (${maxFiles} files). Use --max-files <number> to increase the limit.`);
|
|
186
|
+
}
|
|
175
187
|
const relFilePath = toPosix(path.relative(rootPath, childPath));
|
|
176
188
|
|
|
177
189
|
/** @type {FileNode} */
|
|
@@ -229,15 +241,16 @@ function refineNodeProjectType(pkgPath) {
|
|
|
229
241
|
...pkg.peerDependencies,
|
|
230
242
|
};
|
|
231
243
|
|
|
232
|
-
if (deps['next'])
|
|
233
|
-
if (deps['
|
|
234
|
-
if (deps['
|
|
244
|
+
if (deps['next']) return 'Next.js';
|
|
245
|
+
if (deps['nuxt'] || deps['nuxt3']) return 'Nuxt.js';
|
|
246
|
+
if (deps['react']) return 'React';
|
|
247
|
+
if (deps['vue']) return 'Vue.js';
|
|
235
248
|
if (deps['@angular/core']) return 'Angular';
|
|
236
|
-
if (deps['svelte'])
|
|
237
|
-
if (deps['express'])
|
|
238
|
-
if (deps['fastify'])
|
|
239
|
-
if (deps['koa'])
|
|
240
|
-
if (deps['typescript'])
|
|
249
|
+
if (deps['svelte']) return 'Svelte';
|
|
250
|
+
if (deps['express']) return 'Node.js / Express';
|
|
251
|
+
if (deps['fastify']) return 'Node.js / Fastify';
|
|
252
|
+
if (deps['koa']) return 'Node.js / Koa';
|
|
253
|
+
if (deps['typescript']) return 'Node.js / TypeScript';
|
|
241
254
|
} catch {
|
|
242
255
|
// Malformed package.json — fall through.
|
|
243
256
|
}
|
|
@@ -288,8 +301,18 @@ function findEntryPoints(projectType, flatFiles, rootPath) {
|
|
|
288
301
|
if (validSet.has(f)) { entries.push(f); break; }
|
|
289
302
|
}
|
|
290
303
|
}
|
|
291
|
-
} else if (
|
|
292
|
-
|
|
304
|
+
} else if (
|
|
305
|
+
projectType === 'React' || projectType === 'Next.js' ||
|
|
306
|
+
projectType === 'Nuxt.js' || projectType === 'Vue.js' ||
|
|
307
|
+
projectType === 'Angular' || projectType === 'Svelte'
|
|
308
|
+
) {
|
|
309
|
+
const priorities = [
|
|
310
|
+
'src/main.tsx', 'src/main.jsx', 'src/main.ts', 'src/main.js',
|
|
311
|
+
'pages/_app.tsx', 'pages/_app.js',
|
|
312
|
+
'app/layout.tsx', 'app/layout.js',
|
|
313
|
+
'src/App.tsx', 'src/App.jsx',
|
|
314
|
+
'index.html',
|
|
315
|
+
];
|
|
293
316
|
for (const p of priorities) {
|
|
294
317
|
if (validSet.has(p)) { entries.push(p); break; }
|
|
295
318
|
}
|
|
@@ -311,7 +334,7 @@ function findEntryPoints(projectType, flatFiles, rootPath) {
|
|
|
311
334
|
}
|
|
312
335
|
}
|
|
313
336
|
} else if (projectType.includes('Python')) {
|
|
314
|
-
const priorities = ['main.py', 'app.py', '__main__.py'];
|
|
337
|
+
const priorities = ['main.py', 'app.py', '__main__.py', 'manage.py', 'run.py'];
|
|
315
338
|
for (const p of priorities) {
|
|
316
339
|
if (validSet.has(p)) { entries.push(p); break; }
|
|
317
340
|
}
|
|
@@ -328,25 +351,52 @@ function findEntryPoints(projectType, flatFiles, rootPath) {
|
|
|
328
351
|
}
|
|
329
352
|
}
|
|
330
353
|
}
|
|
354
|
+
} else if (projectType === 'Go') {
|
|
355
|
+
const priorities = ['main.go', 'cmd/main.go', 'cmd/api/main.go'];
|
|
356
|
+
for (const p of priorities) {
|
|
357
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
358
|
+
}
|
|
359
|
+
} else if (projectType === 'Rust') {
|
|
360
|
+
const priorities = ['src/main.rs', 'src/lib.rs'];
|
|
361
|
+
for (const p of priorities) {
|
|
362
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
363
|
+
}
|
|
364
|
+
} else if (projectType === 'Ruby') {
|
|
365
|
+
const priorities = ['app.rb', 'main.rb', 'config.ru', 'Rakefile'];
|
|
366
|
+
for (const p of priorities) {
|
|
367
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
368
|
+
}
|
|
369
|
+
} else if (projectType === 'PHP / Composer') {
|
|
370
|
+
const priorities = ['index.php', 'public/index.php', 'src/index.php', 'artisan'];
|
|
371
|
+
for (const p of priorities) {
|
|
372
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
373
|
+
}
|
|
374
|
+
} else if (projectType === 'Elixir') {
|
|
375
|
+
const priorities = ['lib/mix/tasks/run.ex', 'mix.exs'];
|
|
376
|
+
for (const p of priorities) {
|
|
377
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
378
|
+
}
|
|
331
379
|
}
|
|
332
380
|
|
|
333
381
|
if (entries.length === 0) {
|
|
334
|
-
// Fallbacks for Unknown or
|
|
335
|
-
|
|
336
|
-
let maxScore = -1;
|
|
382
|
+
// Fallbacks for Unknown or unrecognised projects.
|
|
383
|
+
// First try well-known index file patterns.
|
|
337
384
|
for (const f of validFiles) {
|
|
338
385
|
if (/^(src\/)?index\.[a-z]+$/.test(f)) {
|
|
339
386
|
entries.push(f);
|
|
340
387
|
break;
|
|
341
388
|
}
|
|
342
389
|
}
|
|
390
|
+
// If still empty, pick the file with the highest import/export density.
|
|
343
391
|
if (entries.length === 0) {
|
|
392
|
+
let best = null;
|
|
393
|
+
let maxScore = -1;
|
|
344
394
|
for (const f of validFiles) {
|
|
345
|
-
if (f.match(/\.(js|ts|jsx|tsx|py|java|go|rb|php)$/)) {
|
|
395
|
+
if (f.match(/\.(js|ts|jsx|tsx|py|java|go|rs|rb|php)$/)) {
|
|
346
396
|
try {
|
|
347
397
|
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
348
|
-
const score = (content.match(/import /g) || []).length +
|
|
349
|
-
(content.match(/export /g) || []).length +
|
|
398
|
+
const score = (content.match(/import /g) || []).length +
|
|
399
|
+
(content.match(/export /g) || []).length +
|
|
350
400
|
(content.match(/require\(/g) || []).length;
|
|
351
401
|
if (score > maxScore) {
|
|
352
402
|
maxScore = score;
|
|
@@ -389,6 +439,7 @@ function countFolders(node) {
|
|
|
389
439
|
export function scan(targetPath, options = {}) {
|
|
390
440
|
const rootPath = path.resolve(targetPath);
|
|
391
441
|
const includeHidden = !!options.includeHidden;
|
|
442
|
+
const maxFiles = options.maxFiles || 50000; // Default cap of 50k files for huge repos
|
|
392
443
|
|
|
393
444
|
// Validate target
|
|
394
445
|
let stat;
|
|
@@ -409,7 +460,7 @@ export function scan(targetPath, options = {}) {
|
|
|
409
460
|
let totalFolders = 0;
|
|
410
461
|
|
|
411
462
|
if (stat.isDirectory()) {
|
|
412
|
-
tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden);
|
|
463
|
+
tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden, maxFiles);
|
|
413
464
|
projectType = detectProjectType(rootPath);
|
|
414
465
|
// Count all directory nodes in the tree (excluding root itself).
|
|
415
466
|
totalFolders = countFolders(tree) - 1;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Toren — Tree Renderer
|
|
3
|
-
*
|
|
4
|
-
* Formats a flat array of file paths into a clean CLI tree view.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Converts a flat file list into a hierarchical tree structure and renders it.
|
|
9
|
-
*
|
|
10
|
-
* @param {Array<string>} flatFiles - Array of relative file paths
|
|
11
|
-
* @returns {{ output: string, totalFilesShown: number }}
|
|
12
|
-
*/
|
|
13
|
-
export function renderTree(flatFiles) {
|
|
14
|
-
const root = { type: 'directory', children: {} };
|
|
15
|
-
|
|
16
|
-
// 1. Build the tree structure
|
|
17
|
-
for (const p of flatFiles) {
|
|
18
|
-
const parts = p.split(/[/\\]/).filter(Boolean);
|
|
19
|
-
|
|
20
|
-
let current = root;
|
|
21
|
-
for (let i = 0; i < parts.length; i++) {
|
|
22
|
-
const part = parts[i];
|
|
23
|
-
const isFile = i === parts.length - 1;
|
|
24
|
-
|
|
25
|
-
if (!current.children[part]) {
|
|
26
|
-
current.children[part] = isFile
|
|
27
|
-
? { type: 'file', name: part }
|
|
28
|
-
: { type: 'directory', name: part, children: {} };
|
|
29
|
-
}
|
|
30
|
-
current = current.children[part];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const state = { output: [], filesCount: 0 };
|
|
35
|
-
|
|
36
|
-
// 2. Traverse and format the tree (depth limit 4)
|
|
37
|
-
function traverse(node, depth) {
|
|
38
|
-
if (depth >= 4) return;
|
|
39
|
-
|
|
40
|
-
const children = Object.values(node.children || {}).sort((a, b) => {
|
|
41
|
-
// Directories first, then files
|
|
42
|
-
if (a.type !== b.type) {
|
|
43
|
-
return a.type === 'directory' ? -1 : 1;
|
|
44
|
-
}
|
|
45
|
-
// Alphabetical sort
|
|
46
|
-
return a.name.localeCompare(b.name);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
for (const child of children) {
|
|
50
|
-
const indent = ' '.repeat(depth);
|
|
51
|
-
if (child.type === 'directory') {
|
|
52
|
-
state.output.push(`${indent}📁 ${child.name}`);
|
|
53
|
-
traverse(child, depth + 1);
|
|
54
|
-
} else {
|
|
55
|
-
state.output.push(`${indent}└── ${child.name}`);
|
|
56
|
-
state.filesCount++;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
traverse(root, 0);
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
output: state.output.join('\n'),
|
|
65
|
-
totalFilesShown: state.filesCount
|
|
66
|
-
};
|
|
67
|
-
}
|