@lakindu_perera/toren 1.0.1 → 1.0.3
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 +110 -244
- package/bin/toren.js +89 -28
- package/package.json +17 -13
- package/src/renderers/console-renderer.js +18 -6
- package/src/scanner/scan.js +208 -50
- package/src/renderers/tree-renderer.js +0 -67
package/README.md
CHANGED
|
@@ -1,354 +1,220 @@
|
|
|
1
|
-
# Toren
|
|
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
|
|
15
|
-
|
|
16
|
-
Drop it into any unfamiliar repository and immediately see:
|
|
17
|
-
|
|
18
|
-
- What technology stack the project uses
|
|
19
|
-
- Where the application starts (entry points)
|
|
20
|
-
- How many files and folders exist
|
|
21
|
-
- A visual preview of the directory structure
|
|
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.
|
|
22
15
|
|
|
23
|
-
|
|
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.
|
|
24
17
|
|
|
25
18
|
---
|
|
26
19
|
|
|
27
20
|
## Features
|
|
28
21
|
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **Smart ignore rules** — skips `node_modules`, `.git`, `dist`, `build`, `.venv`, and more
|
|
37
|
-
- **Extensible renderer architecture** — add new output formats without touching core logic
|
|
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`).
|
|
38
29
|
|
|
39
30
|
---
|
|
40
31
|
|
|
41
32
|
## Installation
|
|
42
33
|
|
|
43
|
-
Install globally
|
|
34
|
+
Install Toren globally via npm to make the **repository inspection CLI** available anywhere on your machine:
|
|
44
35
|
|
|
45
36
|
```bash
|
|
46
37
|
npm install -g @lakindu_perera/toren
|
|
47
38
|
```
|
|
48
39
|
|
|
49
|
-
Or run without installing:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
npx @lakindu_perera/toren
|
|
53
|
-
```
|
|
54
|
-
|
|
55
40
|
**Requirements:** Node.js 18.0.0 or higher.
|
|
56
41
|
|
|
57
42
|
---
|
|
58
43
|
|
|
59
|
-
##
|
|
44
|
+
## CLI Usage
|
|
60
45
|
|
|
61
|
-
|
|
46
|
+
Toren is designed to be simple and intuitive. Point it at any directory to generate an immediate intelligence report.
|
|
62
47
|
|
|
63
48
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
# Scan a specific path
|
|
50
|
+
toren /path/to/project
|
|
66
51
|
|
|
67
|
-
Scan
|
|
68
|
-
|
|
69
|
-
```bash
|
|
52
|
+
# Scan the current directory
|
|
70
53
|
toren .
|
|
71
|
-
toren ../my-project
|
|
72
|
-
toren /path/to/any/repo
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Output results as JSON:
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
toren --json
|
|
79
|
-
|
|
55
|
+
# Export results in different formats
|
|
56
|
+
toren --format json
|
|
57
|
+
toren --format markdown
|
|
58
|
+
toren --format html
|
|
80
59
|
|
|
81
|
-
|
|
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
|
|
82
64
|
|
|
83
|
-
|
|
65
|
+
# Lifecycle & Help Commands
|
|
66
|
+
toren --help
|
|
67
|
+
toren --version
|
|
84
68
|
toren --doctor
|
|
69
|
+
toren --uninstall
|
|
85
70
|
```
|
|
86
71
|
|
|
72
|
+
### All Flags
|
|
73
|
+
|
|
74
|
+
| Flag | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `[path]` | Directory to scan. Defaults to the current directory (`.`). |
|
|
77
|
+
| `--format <type>` | Output format: `console` (default), `json`, `markdown`, `html`. |
|
|
78
|
+
| `--include-hidden` | Include hidden files and dot-directories in the scan. |
|
|
79
|
+
| `--max-files <N>` | Override the default 50,000-file scan limit. |
|
|
80
|
+
| `--help` / `-h` | Show usage information. |
|
|
81
|
+
| `--version` / `-V` | Print the installed version number. |
|
|
82
|
+
| `--doctor` | Diagnose the global installation health. |
|
|
83
|
+
| `--uninstall` | Safely remove Toren from the global npm environment. |
|
|
84
|
+
|
|
85
|
+
> **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
|
|
86
|
+
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## Output Examples
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
### Console Output Example
|
|
92
|
+
The default `console` format renders a beautiful summary directly in your terminal:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
Toren v1.0.3 — Codebase Onboarding Intelligence
|
|
93
96
|
|
|
94
97
|
🔍 Project Summary
|
|
95
98
|
────────────────────────────────────────────────────────────────────────────────
|
|
96
|
-
Path: ./my-app
|
|
97
|
-
Project type:
|
|
98
|
-
Total files:
|
|
99
|
-
Total folders:
|
|
100
|
-
Scan duration:
|
|
99
|
+
Path: ./my-react-app
|
|
100
|
+
Project type: React
|
|
101
|
+
Total files: 32
|
|
102
|
+
Total folders: 6
|
|
103
|
+
Scan duration: 4 ms
|
|
101
104
|
|
|
102
105
|
🚪 Entry Points
|
|
103
106
|
────────────────────────────────────────────────────────────────────────────────
|
|
104
|
-
→ src/
|
|
105
|
-
→ src/app/layout.tsx
|
|
107
|
+
→ src/main.tsx
|
|
106
108
|
|
|
107
109
|
📁 Folder Structure (first 20 files)
|
|
108
110
|
────────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
my-app/
|
|
111
|
+
my-react-app/
|
|
110
112
|
├── public/
|
|
111
|
-
│ └──
|
|
113
|
+
│ └── vite.svg
|
|
112
114
|
├── src/
|
|
113
|
-
│ ├──
|
|
114
|
-
│ │
|
|
115
|
-
│
|
|
116
|
-
│ ├──
|
|
117
|
-
│
|
|
118
|
-
│
|
|
119
|
-
|
|
120
|
-
│ └── utils.ts
|
|
121
|
-
├── .eslintrc.json
|
|
122
|
-
├── next.config.js
|
|
115
|
+
│ ├── assets/
|
|
116
|
+
│ │ └── react.svg
|
|
117
|
+
│ ├── App.css
|
|
118
|
+
│ ├── App.tsx
|
|
119
|
+
│ ├── index.css
|
|
120
|
+
│ └── main.tsx
|
|
121
|
+
├── index.html
|
|
123
122
|
├── package.json
|
|
124
|
-
└──
|
|
125
|
-
… and
|
|
123
|
+
└── vite.config.ts
|
|
124
|
+
… and 21 more file(s) not shown
|
|
126
125
|
|
|
127
126
|
────────────────────────────────────────────────────────────────────────────────
|
|
128
127
|
✅ Scan complete.
|
|
129
128
|
```
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
## JSON Output
|
|
134
|
-
|
|
135
|
-
Use `toren --json` to get a machine-readable result suitable for piping into other tools:
|
|
130
|
+
### JSON Output Example
|
|
131
|
+
Generate machine-readable output for scripts, toolchains, or AI context windows by running `toren --format json`:
|
|
136
132
|
|
|
137
133
|
```json
|
|
138
134
|
{
|
|
139
135
|
"project": {
|
|
140
|
-
"path": "./my-app",
|
|
141
|
-
"type": "
|
|
142
|
-
"framework": "
|
|
136
|
+
"path": "./my-react-app",
|
|
137
|
+
"type": "React",
|
|
138
|
+
"framework": "React"
|
|
143
139
|
},
|
|
144
140
|
"summary": {
|
|
145
|
-
"totalFiles":
|
|
146
|
-
"totalFolders":
|
|
147
|
-
"scanDurationMs":
|
|
141
|
+
"totalFiles": 32,
|
|
142
|
+
"totalFolders": 6,
|
|
143
|
+
"scanDurationMs": 4
|
|
148
144
|
},
|
|
149
145
|
"entryPoints": [
|
|
150
|
-
"src/
|
|
151
|
-
"src/app/layout.tsx"
|
|
146
|
+
"src/main.tsx"
|
|
152
147
|
],
|
|
153
148
|
"structure": [
|
|
154
149
|
{
|
|
155
150
|
"type": "folder",
|
|
156
151
|
"name": "src",
|
|
157
152
|
"children": [
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
"name": "app",
|
|
161
|
-
"children": [
|
|
162
|
-
{ "type": "file", "name": "layout.tsx" },
|
|
163
|
-
{ "type": "file", "name": "page.tsx" }
|
|
164
|
-
]
|
|
165
|
-
}
|
|
153
|
+
{ "type": "file", "name": "App.tsx" },
|
|
154
|
+
{ "type": "file", "name": "main.tsx" }
|
|
166
155
|
]
|
|
167
156
|
},
|
|
168
|
-
{ "type": "file", "name": "package.json" }
|
|
169
|
-
{ "type": "file", "name": "next.config.js" }
|
|
157
|
+
{ "type": "file", "name": "package.json" }
|
|
170
158
|
]
|
|
171
159
|
}
|
|
172
160
|
```
|
|
173
161
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
## Supported Project Types
|
|
177
|
-
|
|
178
|
-
Toren detects the following project types automatically:
|
|
179
|
-
|
|
180
|
-
| Marker File | Detected Type |
|
|
181
|
-
|--------------------------------------|----------------------------|
|
|
182
|
-
| `package.json` | Node.js / JavaScript |
|
|
183
|
-
| `package.json` + `next` dep | Next.js |
|
|
184
|
-
| `package.json` + `react` dep | React |
|
|
185
|
-
| `package.json` + `vue` dep | Vue.js |
|
|
186
|
-
| `package.json` + `@angular/core` dep | Angular |
|
|
187
|
-
| `package.json` + `svelte` dep | Svelte |
|
|
188
|
-
| `package.json` + `express` dep | Node.js / Express |
|
|
189
|
-
| `package.json` + `fastify` dep | Node.js / Fastify |
|
|
190
|
-
| `package.json` + `koa` dep | Node.js / Koa |
|
|
191
|
-
| `package.json` + `typescript` dep | Node.js / TypeScript |
|
|
192
|
-
| `requirements.txt` | Python |
|
|
193
|
-
| `Pipfile` | Python (Pipenv) |
|
|
194
|
-
| `pyproject.toml` | Python (pyproject) |
|
|
195
|
-
| `go.mod` | Go |
|
|
196
|
-
| `Cargo.toml` | Rust |
|
|
197
|
-
| `pom.xml` | Java / Spring Boot |
|
|
198
|
-
| `build.gradle` | Java / Gradle |
|
|
199
|
-
| `composer.json` | PHP / Composer |
|
|
200
|
-
| `Gemfile` | Ruby |
|
|
201
|
-
| `mix.exs` | Elixir |
|
|
202
|
-
|
|
203
|
-
If no marker is found, Toren reports `Unknown` without failing.
|
|
162
|
+
*Note: You can also generate rich, GitHub-flavored Markdown (`--format markdown`) or self-contained HTML reports (`--format html`) for documentation purposes!*
|
|
204
163
|
|
|
205
164
|
---
|
|
206
165
|
|
|
207
|
-
##
|
|
208
|
-
|
|
209
|
-
| Command | Description |
|
|
210
|
-
|----------------------|--------------------------------------------------|
|
|
211
|
-
| `toren` | Scan the current directory |
|
|
212
|
-
| `toren [path]` | Scan a specific directory or file path |
|
|
213
|
-
| `toren --json` | Output scan results as formatted JSON |
|
|
214
|
-
| `toren --version` | Print the installed version number |
|
|
215
|
-
| `toren --help` | Show usage information |
|
|
216
|
-
| `toren --doctor` | Diagnose the global installation health |
|
|
217
|
-
| `toren --uninstall` | Guided removal of the global installation |
|
|
218
|
-
|
|
219
|
-
---
|
|
166
|
+
## Architecture
|
|
220
167
|
|
|
221
|
-
|
|
168
|
+
Toren's internal architecture emphasizes modular design, separation of concerns, and a strict **zero dependency** philosophy.
|
|
222
169
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
├── src/
|
|
228
|
-
│ ├── lifecycle.js # --doctor and --uninstall command implementations
|
|
229
|
-
│ ├── scanner/
|
|
230
|
-
│ │ └── scan.js # Core scanning engine — file walker, type detection, entry point detection
|
|
231
|
-
│ └── renderers/
|
|
232
|
-
│ ├── console-renderer.js # ANSI-styled terminal output
|
|
233
|
-
│ ├── json-renderer.js # Machine-readable JSON output
|
|
234
|
-
│ └── tree-renderer.js # Standalone flat-file tree formatter (utility)
|
|
235
|
-
└── package.json
|
|
236
|
-
```
|
|
170
|
+
- **`bin/toren.js`** — The CLI entry point. Handles argument parsing, option validation, and orchestrates the scanning and rendering phases.
|
|
171
|
+
- **`src/scanner/scan.js`** — The core scanning engine. Safely traverses the file system, executes framework detection heuristics, and extracts entry points.
|
|
172
|
+
- **`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.
|
|
173
|
+
- **`src/lifecycle.js`** — Dedicated install/uninstall diagnostic tools (`--doctor`, `--uninstall`) to ensure the global CLI binary remains healthy.
|
|
237
174
|
|
|
238
175
|
---
|
|
239
176
|
|
|
240
|
-
##
|
|
241
|
-
|
|
242
|
-
### 1. Scanner
|
|
243
|
-
|
|
244
|
-
`src/scanner/scan.js` is the core engine. It takes a target path and recursively walks the file system using Node's `fs.readdirSync`, collecting every file and directory while skipping entries in the ignore list (`node_modules`, `.git`, `dist`, `build`, etc.).
|
|
177
|
+
## Why Toren Exists
|
|
245
178
|
|
|
246
|
-
|
|
179
|
+
Modern software development moves fast, but **onboarding into large codebases is slow**.
|
|
247
180
|
|
|
248
|
-
|
|
181
|
+
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.
|
|
249
182
|
|
|
250
|
-
|
|
183
|
+
**Toren solves the "first 5 minutes of any repo" problem.**
|
|
251
184
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
During the walk, each filename is checked against a known set of entry points: `index.js`, `index.ts`, `main.py`, `App.tsx`, `Application.java`, `page.tsx`, `layout.tsx`, etc. All matches are collected and surfaced in the output.
|
|
255
|
-
|
|
256
|
-
### 4. Renderer
|
|
257
|
-
|
|
258
|
-
The scan result — a plain JavaScript object — is passed to a renderer. Renderers are completely decoupled from the scanner; they only read data and produce output.
|
|
259
|
-
|
|
260
|
-
### 5. Output
|
|
261
|
-
|
|
262
|
-
The CLI selects the appropriate renderer based on flags (`--json` → JSON renderer; default → console renderer). Errors are caught and formatted consistently in both modes.
|
|
185
|
+
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.
|
|
263
186
|
|
|
264
187
|
---
|
|
265
188
|
|
|
266
|
-
##
|
|
189
|
+
## Roadmap (Future Improvements)
|
|
267
190
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
Argument Parser
|
|
273
|
-
│
|
|
274
|
-
▼
|
|
275
|
-
Scanner (scan.js)
|
|
276
|
-
│
|
|
277
|
-
├── Directory Walker
|
|
278
|
-
├── Ignore Filter
|
|
279
|
-
├── Framework Detector
|
|
280
|
-
└── Entry Point Detector
|
|
281
|
-
│
|
|
282
|
-
▼
|
|
283
|
-
ScanResult (plain object)
|
|
284
|
-
│
|
|
285
|
-
├──────────────────┐
|
|
286
|
-
▼ ▼
|
|
287
|
-
Console Renderer JSON Renderer
|
|
288
|
-
(ANSI terminal) (stdout / pipe)
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
Adding a new output format is as simple as creating a new file in `src/renderers/` and importing it in `bin/toren.js`.
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## Why Toren?
|
|
296
|
-
|
|
297
|
-
When you encounter a new codebase, the usual approach is to start opening files, guessing at folder names, and reading `package.json` manually. This works — but it's slow and inconsistent.
|
|
298
|
-
|
|
299
|
-
Toren automates that first pass. In one command, you get a structured summary of what the project is, where it starts, and what's inside. This is especially useful when:
|
|
300
|
-
|
|
301
|
-
- **Onboarding to a new job** — quickly orient yourself before your first meeting
|
|
302
|
-
- **Reviewing a pull request or open-source repo** — understand the scope at a glance
|
|
303
|
-
- **Auditing a legacy codebase** — know what you're dealing with before diving in
|
|
304
|
-
- **Building tooling** — use `--json` to feed project metadata into scripts or AI tools
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Roadmap
|
|
309
|
-
|
|
310
|
-
- [x] Console renderer
|
|
311
|
-
- [x] JSON renderer
|
|
312
|
-
- [x] Framework detection (11+ project types)
|
|
313
|
-
- [x] Entry point detection
|
|
314
|
-
- [x] `--doctor` install health check
|
|
315
|
-
- [x] `--uninstall` guided removal
|
|
316
|
-
- [x] Markdown renderer (`--format markdown`)
|
|
317
|
-
- [x] HTML renderer (`--format html`)
|
|
318
|
-
- [ ] YAML output
|
|
319
|
-
- [ ] `.torenignore` configuration file
|
|
320
|
-
- [ ] Plugin / custom renderer system
|
|
321
|
-
- [ ] Dependency graph analysis
|
|
322
|
-
- [ ] AI-generated project summary
|
|
323
|
-
- [ ] Project health score
|
|
324
|
-
- [ ] Architecture visualisation layer
|
|
191
|
+
- `.torenignore` configuration file support
|
|
192
|
+
- Dependency graph analysis and visualization
|
|
193
|
+
- Plugin system for custom renderer injection
|
|
194
|
+
- AI-generated natural language project summaries
|
|
325
195
|
|
|
326
196
|
---
|
|
327
197
|
|
|
328
198
|
## Contributing
|
|
329
199
|
|
|
330
|
-
Contributions are welcome
|
|
331
|
-
|
|
332
|
-
1. **Fork** this repository
|
|
333
|
-
2. **Create** a feature branch: `git checkout -b feature/my-feature`
|
|
334
|
-
3. **Commit** your changes: `git commit -m "feat: add my feature"`
|
|
335
|
-
4. **Push** to your branch: `git push origin feature/my-feature`
|
|
336
|
-
5. **Open** a pull request
|
|
200
|
+
Contributions to improve this codebase analyzer CLI are always welcome!
|
|
337
201
|
|
|
338
|
-
|
|
202
|
+
1. Fork the repository
|
|
203
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
204
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
205
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
206
|
+
5. Open a Pull Request
|
|
339
207
|
|
|
340
|
-
|
|
341
|
-
- Zero external runtime dependencies — keep it that way
|
|
342
|
-
- No TypeScript compilation step — plain ES modules only
|
|
343
|
-
- Keep scanner and renderers strictly decoupled
|
|
344
|
-
- Document public functions with JSDoc
|
|
208
|
+
*Please ensure all new features maintain the strict zero-dependency architecture rule.*
|
|
345
209
|
|
|
346
210
|
---
|
|
347
211
|
|
|
348
|
-
## License
|
|
212
|
+
## 📄 License
|
|
349
213
|
|
|
350
|
-
|
|
214
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
351
215
|
|
|
352
216
|
---
|
|
353
217
|
|
|
354
|
-
|
|
218
|
+
## 👨💻 Author
|
|
219
|
+
|
|
220
|
+
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
|
* ──────────────────────────
|
|
@@ -42,12 +44,14 @@ function printHelp() {
|
|
|
42
44
|
const formatList = SUPPORTED_FORMATS.map(f => ` ${f}`).join('\n');
|
|
43
45
|
console.log(`
|
|
44
46
|
\x1b[1mUsage:\x1b[0m
|
|
45
|
-
toren [path]
|
|
46
|
-
toren --format <type>
|
|
47
|
-
toren --
|
|
48
|
-
toren --
|
|
49
|
-
toren --
|
|
50
|
-
toren --
|
|
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
|
|
51
55
|
|
|
52
56
|
\x1b[1mOutput Formats:\x1b[0m
|
|
53
57
|
${formatList}
|
|
@@ -55,10 +59,13 @@ ${formatList}
|
|
|
55
59
|
console is the default.
|
|
56
60
|
|
|
57
61
|
\x1b[1mExamples:\x1b[0m
|
|
58
|
-
toren .
|
|
59
|
-
toren ./my-project
|
|
60
|
-
toren --format json
|
|
61
|
-
toren --format
|
|
62
|
+
toren . Scan the current directory
|
|
63
|
+
toren ./my-project Scan a specific project folder
|
|
64
|
+
toren --format json . Output results as JSON
|
|
65
|
+
toren --format markdown . > out.md Save a Markdown report to a file
|
|
66
|
+
toren --format html . > out.html Save an HTML report to a file
|
|
67
|
+
toren --include-hidden . Include hidden dot-files in scan
|
|
68
|
+
toren --max-files 100000 . Override the 50k file scan limit
|
|
62
69
|
`);
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -69,8 +76,10 @@ ${formatList}
|
|
|
69
76
|
/**
|
|
70
77
|
* @typedef {Object} ParsedArgs
|
|
71
78
|
* @property {'exit'|'wait'|'scan'} action
|
|
72
|
-
* @property {string} [target]
|
|
73
|
-
* @property {string} [format]
|
|
79
|
+
* @property {string} [target] - Resolved path to scan
|
|
80
|
+
* @property {string} [format] - Renderer format name
|
|
81
|
+
* @property {boolean} [includeHidden] - Whether to include hidden files
|
|
82
|
+
* @property {number} [maxFiles] - Max files to scan
|
|
74
83
|
*/
|
|
75
84
|
|
|
76
85
|
/**
|
|
@@ -88,18 +97,18 @@ function parseArgs() {
|
|
|
88
97
|
// ── Informational flags ─────────────────────────────────────────────────
|
|
89
98
|
if (args.includes('--help') || args.includes('-h')) {
|
|
90
99
|
printHelp();
|
|
91
|
-
return { action: 'exit' };
|
|
100
|
+
return { action: 'exit', code: 0 };
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
if (args.includes('--version') || args.includes('-
|
|
103
|
+
if (args.includes('--version') || args.includes('-V') || args.includes('-v')) {
|
|
95
104
|
console.log(pkg.version);
|
|
96
|
-
return { action: 'exit' };
|
|
105
|
+
return { action: 'exit', code: 0 };
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
// ── Lifecycle commands ──────────────────────────────────────────────────
|
|
100
109
|
if (args.includes('--doctor')) {
|
|
101
110
|
runDoctor(pkg.version);
|
|
102
|
-
return { action: 'exit' };
|
|
111
|
+
return { action: 'exit', code: 0 };
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
if (args.includes('--uninstall')) {
|
|
@@ -114,36 +123,82 @@ function parseArgs() {
|
|
|
114
123
|
|
|
115
124
|
const formatIdx = args.indexOf('--format');
|
|
116
125
|
if (formatIdx !== -1) {
|
|
117
|
-
|
|
118
|
-
// If the
|
|
119
|
-
|
|
126
|
+
const nextToken = args[formatIdx + 1];
|
|
127
|
+
// If the next token is missing or starts with '-', the user omitted the value.
|
|
128
|
+
if (nextToken === undefined || nextToken.startsWith('-')) {
|
|
129
|
+
console.error('');
|
|
130
|
+
console.error('\x1b[31m --format requires a value.\x1b[0m');
|
|
131
|
+
console.error('');
|
|
132
|
+
console.error(` Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
|
|
133
|
+
console.error('');
|
|
134
|
+
console.error(' Run \x1b[36mtoren --help\x1b[0m for usage.');
|
|
135
|
+
console.error('');
|
|
136
|
+
return { action: 'exit', code: 1 };
|
|
137
|
+
}
|
|
138
|
+
format = nextToken;
|
|
120
139
|
} else if (args.includes('--json')) {
|
|
121
140
|
format = 'json';
|
|
122
141
|
}
|
|
123
142
|
|
|
143
|
+
// ── Hidden files ────────────────────────────────────────────────────────
|
|
144
|
+
const includeHidden = args.includes('--include-hidden');
|
|
145
|
+
|
|
124
146
|
// ── Target path ─────────────────────────────────────────────────────────
|
|
125
147
|
// Build the set of tokens that are consumed as values by named flags so
|
|
126
148
|
// we don't accidentally treat them as the positional path argument.
|
|
127
|
-
// Currently only --format consumes a value token.
|
|
128
149
|
const consumedValues = new Set();
|
|
129
|
-
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined) {
|
|
150
|
+
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined && !args[formatIdx + 1].startsWith('-')) {
|
|
130
151
|
consumedValues.add(args[formatIdx + 1]);
|
|
131
152
|
}
|
|
132
153
|
|
|
154
|
+
// ── Max files ───────────────────────────────────────────────────────────
|
|
155
|
+
let maxFiles = undefined;
|
|
156
|
+
const maxFilesIdx = args.indexOf('--max-files');
|
|
157
|
+
if (maxFilesIdx !== -1) {
|
|
158
|
+
const rawVal = args[maxFilesIdx + 1];
|
|
159
|
+
if (rawVal === undefined || rawVal.startsWith('-')) {
|
|
160
|
+
console.error('');
|
|
161
|
+
console.error('\x1b[31m --max-files requires a numeric value.\x1b[0m');
|
|
162
|
+
console.error(' Example: toren --max-files 100000');
|
|
163
|
+
console.error('');
|
|
164
|
+
return { action: 'exit', code: 1 };
|
|
165
|
+
}
|
|
166
|
+
maxFiles = parseInt(rawVal, 10);
|
|
167
|
+
if (isNaN(maxFiles) || maxFiles < 1) {
|
|
168
|
+
console.error('');
|
|
169
|
+
console.error(`\x1b[31m --max-files must be a positive integer, got: ${rawVal}\x1b[0m`);
|
|
170
|
+
console.error('');
|
|
171
|
+
return { action: 'exit', code: 1 };
|
|
172
|
+
}
|
|
173
|
+
consumedValues.add(rawVal);
|
|
174
|
+
}
|
|
175
|
+
|
|
133
176
|
// First non-flag, non-consumed token is the target path; default to cwd.
|
|
134
177
|
const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
|
|
135
178
|
|
|
136
179
|
// ── Unknown flag check ──────────────────────────────────────────────────
|
|
137
|
-
const knownFlags = new Set([
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
const knownFlags = new Set([
|
|
181
|
+
'--help', '-h',
|
|
182
|
+
'--version', '-V', '-v',
|
|
183
|
+
'--doctor',
|
|
184
|
+
'--uninstall',
|
|
185
|
+
'--format',
|
|
186
|
+
'--json',
|
|
187
|
+
'--include-hidden',
|
|
188
|
+
'--max-files',
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const unknownFlag = args.find(
|
|
192
|
+
a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a),
|
|
193
|
+
);
|
|
194
|
+
|
|
140
195
|
if (unknownFlag) {
|
|
141
196
|
console.error(`\x1b[31m Unknown flag: ${unknownFlag}\x1b[0m`);
|
|
142
197
|
console.error(` Run \x1b[36mtoren --help\x1b[0m for usage.\n`);
|
|
143
|
-
return { action: 'exit' };
|
|
198
|
+
return { action: 'exit', code: 1 };
|
|
144
199
|
}
|
|
145
200
|
|
|
146
|
-
return { action: 'scan', target, format };
|
|
201
|
+
return { action: 'scan', target, format, includeHidden, maxFiles };
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
// ---------------------------------------------------------------------------
|
|
@@ -178,7 +233,10 @@ function assertValidFormat(format) {
|
|
|
178
233
|
|
|
179
234
|
(function main() {
|
|
180
235
|
const parsed = parseArgs();
|
|
181
|
-
|
|
236
|
+
|
|
237
|
+
if (parsed.action === 'exit') {
|
|
238
|
+
process.exit(parsed.code ?? 0);
|
|
239
|
+
}
|
|
182
240
|
if (parsed.action === 'wait') return;
|
|
183
241
|
|
|
184
242
|
// Validate before scanning — fail fast on bad format names.
|
|
@@ -187,7 +245,10 @@ function assertValidFormat(format) {
|
|
|
187
245
|
const render = renderers[parsed.format];
|
|
188
246
|
|
|
189
247
|
try {
|
|
190
|
-
const result = scan(parsed.target
|
|
248
|
+
const result = scan(parsed.target, {
|
|
249
|
+
includeHidden: parsed.includeHidden,
|
|
250
|
+
maxFiles: parsed.maxFiles,
|
|
251
|
+
});
|
|
191
252
|
render(result, { cwd: process.cwd() });
|
|
192
253
|
} catch (err) {
|
|
193
254
|
// Render errors in the requested format where possible.
|
package/package.json
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakindu_perera/toren",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A CLI
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
-
"toren": "
|
|
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
|
-
"cli",
|
|
21
|
-
"codebase",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
20
|
+
"cli tool",
|
|
21
|
+
"codebase scanner",
|
|
22
|
+
"repository analyzer",
|
|
23
|
+
"project structure viewer",
|
|
24
|
+
"framework detection",
|
|
25
|
+
"monorepo analyzer",
|
|
26
|
+
"node cli tool",
|
|
27
|
+
"developer tool",
|
|
28
|
+
"onboarding"
|
|
25
29
|
],
|
|
26
30
|
"author": "Lakindu Perera",
|
|
27
31
|
"repository": {
|
|
28
32
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/lakindudev/toren.git"
|
|
33
|
+
"url": "git+https://github.com/lakindudev/toren.git"
|
|
30
34
|
},
|
|
31
35
|
"bugs": {
|
|
32
36
|
"url": "https://github.com/lakindudev/toren/issues"
|
|
@@ -98,10 +98,10 @@ function row(label, value, ...valueCodes) {
|
|
|
98
98
|
* @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
|
|
99
99
|
* @param {string} prefix - Accumulated indentation
|
|
100
100
|
* @param {boolean} isLast - Whether this is the last sibling
|
|
101
|
-
* @param {{ count: number }} counter - Shared mutable file counter
|
|
101
|
+
* @param {{ count: number, maxReached: boolean }} counter - Shared mutable file counter
|
|
102
102
|
*/
|
|
103
103
|
function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
|
|
104
|
-
if (counter.
|
|
104
|
+
if (counter.maxReached) return;
|
|
105
105
|
if (depth >= maxDepth) return;
|
|
106
106
|
|
|
107
107
|
const connector = isLast ? '└── ' : '├── ';
|
|
@@ -117,8 +117,16 @@ function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
for (let i = 0; i < children.length; i++) {
|
|
120
|
-
if (counter.count >= PREVIEW_LIMIT)
|
|
121
|
-
|
|
120
|
+
if (counter.count >= PREVIEW_LIMIT) {
|
|
121
|
+
console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
|
|
122
|
+
counter.maxReached = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
// Check if this child will be the last one we render due to limits
|
|
126
|
+
let willBeLast = i === children.length - 1;
|
|
127
|
+
|
|
128
|
+
renderTree(children[i], prefix + extension, willBeLast, counter, depth + 1, maxDepth);
|
|
129
|
+
if (counter.maxReached) break;
|
|
122
130
|
}
|
|
123
131
|
} else {
|
|
124
132
|
console.log(`${prefix}${connector}${paint(node.name, C.white)}`);
|
|
@@ -198,11 +206,15 @@ export function render(result, options = {}) {
|
|
|
198
206
|
|
|
199
207
|
console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
|
|
200
208
|
|
|
201
|
-
const counter = { count: 0 };
|
|
209
|
+
const counter = { count: 0, maxReached: false };
|
|
202
210
|
const children = tree.children ?? [];
|
|
203
211
|
for (let i = 0; i < children.length; i++) {
|
|
204
|
-
if (counter.count >= PREVIEW_LIMIT)
|
|
212
|
+
if (counter.count >= PREVIEW_LIMIT) {
|
|
213
|
+
console.log(`└── ${paint('...', C.dim)}`);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
205
216
|
renderTree(children[i], '', i === children.length - 1, counter);
|
|
217
|
+
if (counter.maxReached) break;
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
if (flatFiles.length > PREVIEW_LIMIT) {
|
package/src/scanner/scan.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* - Detect the project type from marker files
|
|
9
9
|
* - Identify known entry-point files
|
|
10
10
|
*
|
|
11
|
-
* This module is intentionally free of side-effects (no console.log)
|
|
11
|
+
* This module is intentionally free of side-effects (no console.log),
|
|
12
|
+
* except for warnings about unreadable directories.
|
|
12
13
|
* All output concerns live in bin/toren.js.
|
|
13
14
|
*
|
|
14
15
|
* Designed to scale into:
|
|
@@ -44,6 +45,10 @@ const IGNORED_DIRS = new Set([
|
|
|
44
45
|
'venv',
|
|
45
46
|
'.idea',
|
|
46
47
|
'.vscode',
|
|
48
|
+
'.svelte-kit',
|
|
49
|
+
'.turbo',
|
|
50
|
+
'.parcel-cache',
|
|
51
|
+
'.DS_Store'
|
|
47
52
|
]);
|
|
48
53
|
|
|
49
54
|
/**
|
|
@@ -65,24 +70,6 @@ const PROJECT_TYPE_MARKERS = [
|
|
|
65
70
|
{ marker: 'mix.exs', label: 'Elixir' },
|
|
66
71
|
];
|
|
67
72
|
|
|
68
|
-
const ENTRY_POINT_EXACT = new Set([
|
|
69
|
-
'index.js', 'index.ts', 'index.jsx', 'index.tsx',
|
|
70
|
-
'main.js', 'main.ts', 'main.jsx', 'main.tsx', 'main.py',
|
|
71
|
-
'app.js', 'app.ts', 'app.jsx', 'app.tsx', 'App.js', 'App.ts', 'App.jsx', 'App.tsx',
|
|
72
|
-
'server.js', 'server.ts', 'server.jsx', 'server.tsx',
|
|
73
|
-
'Application.java', 'Main.java',
|
|
74
|
-
'page.js', 'page.ts', 'page.jsx', 'page.tsx',
|
|
75
|
-
'layout.js', 'layout.ts', 'layout.jsx', 'layout.tsx',
|
|
76
|
-
'_app.js', '_app.ts', '_app.jsx', '_app.tsx',
|
|
77
|
-
'_document.js', '_document.ts', '_document.jsx', '_document.tsx'
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
function isEntryPoint(filename) {
|
|
81
|
-
if (ENTRY_POINT_EXACT.has(filename)) return true;
|
|
82
|
-
if (filename.endsWith('Application.java')) return true;
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
73
|
// ---------------------------------------------------------------------------
|
|
87
74
|
// Types (JSDoc — no TypeScript dependency required)
|
|
88
75
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +79,7 @@ function isEntryPoint(filename) {
|
|
|
92
79
|
* @property {'file'} type
|
|
93
80
|
* @property {string} name - Basename of the file
|
|
94
81
|
* @property {string} fullPath - Absolute path
|
|
95
|
-
* @property {string} relPath - Path relative to the scanned root
|
|
82
|
+
* @property {string} relPath - Path relative to the scanned root (POSIX style)
|
|
96
83
|
*/
|
|
97
84
|
|
|
98
85
|
/**
|
|
@@ -100,7 +87,7 @@ function isEntryPoint(filename) {
|
|
|
100
87
|
* @property {'directory'} type
|
|
101
88
|
* @property {string} name - Basename of the directory
|
|
102
89
|
* @property {string} fullPath - Absolute path
|
|
103
|
-
* @property {string} relPath - Path relative to the scanned root
|
|
90
|
+
* @property {string} relPath - Path relative to the scanned root (POSIX style)
|
|
104
91
|
* @property {Array<FileNode|DirNode>} children
|
|
105
92
|
*/
|
|
106
93
|
|
|
@@ -119,31 +106,41 @@ function isEntryPoint(filename) {
|
|
|
119
106
|
// Internal helpers
|
|
120
107
|
// ---------------------------------------------------------------------------
|
|
121
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a path to use POSIX separators ('/').
|
|
111
|
+
*/
|
|
112
|
+
function toPosix(p) {
|
|
113
|
+
return p.replace(/\\/g, '/');
|
|
114
|
+
}
|
|
115
|
+
|
|
122
116
|
/**
|
|
123
117
|
* Determine whether a directory entry should be skipped.
|
|
124
118
|
*
|
|
125
119
|
* @param {string} name - Basename of the entry
|
|
126
120
|
* @param {fs.Dirent} dirent
|
|
121
|
+
* @param {boolean} includeHidden
|
|
127
122
|
* @returns {boolean}
|
|
128
123
|
*/
|
|
129
|
-
function shouldIgnore(name, dirent) {
|
|
130
|
-
if (name.startsWith('.')
|
|
124
|
+
function shouldIgnore(name, dirent, includeHidden) {
|
|
125
|
+
if (!includeHidden && name.startsWith('.')) return true;
|
|
131
126
|
return IGNORED_DIRS.has(name);
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
/**
|
|
135
130
|
* Recursively walk `dirPath`, building a DirNode tree.
|
|
136
|
-
* Also populates `flatFiles`
|
|
131
|
+
* Also populates `flatFiles` array by reference.
|
|
137
132
|
*
|
|
138
133
|
* @param {string} dirPath - Absolute path of the current directory
|
|
139
134
|
* @param {string} rootPath - Absolute path of the scan root (for relative paths)
|
|
140
|
-
* @param {Array<string>} flatFiles
|
|
141
|
-
* @param {
|
|
135
|
+
* @param {Array<string>} flatFiles - Accumulator for all relative file paths
|
|
136
|
+
* @param {boolean} includeHidden - Whether to include hidden files
|
|
137
|
+
* @param {number} maxFiles - Maximum number of files to scan before aborting
|
|
142
138
|
* @returns {DirNode}
|
|
143
139
|
*/
|
|
144
|
-
function walkDirectory(dirPath, rootPath, flatFiles,
|
|
140
|
+
function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
|
|
145
141
|
const name = path.basename(dirPath);
|
|
146
|
-
|
|
142
|
+
let rawRelPath = path.relative(rootPath, dirPath) || '.';
|
|
143
|
+
const relPath = toPosix(rawRelPath);
|
|
147
144
|
|
|
148
145
|
/** @type {DirNode} */
|
|
149
146
|
const node = {
|
|
@@ -159,6 +156,8 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
159
156
|
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
160
157
|
} catch {
|
|
161
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`);
|
|
162
161
|
return node;
|
|
163
162
|
}
|
|
164
163
|
|
|
@@ -171,15 +170,21 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
171
170
|
});
|
|
172
171
|
|
|
173
172
|
for (const dirent of entries) {
|
|
174
|
-
if (shouldIgnore(dirent.name, dirent)) continue;
|
|
173
|
+
if (shouldIgnore(dirent.name, dirent, includeHidden)) continue;
|
|
175
174
|
|
|
176
175
|
const childPath = path.join(dirPath, dirent.name);
|
|
177
176
|
|
|
177
|
+
// Explicitly skip symbolic links to prevent infinite loops and unsafe traversals
|
|
178
|
+
if (dirent.isSymbolicLink()) continue;
|
|
179
|
+
|
|
178
180
|
if (dirent.isDirectory()) {
|
|
179
|
-
const childNode = walkDirectory(childPath, rootPath, flatFiles,
|
|
181
|
+
const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden, maxFiles);
|
|
180
182
|
node.children.push(childNode);
|
|
181
183
|
} else if (dirent.isFile()) {
|
|
182
|
-
|
|
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
|
+
}
|
|
187
|
+
const relFilePath = toPosix(path.relative(rootPath, childPath));
|
|
183
188
|
|
|
184
189
|
/** @type {FileNode} */
|
|
185
190
|
const fileNode = {
|
|
@@ -191,10 +196,6 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
191
196
|
|
|
192
197
|
node.children.push(fileNode);
|
|
193
198
|
flatFiles.push(relFilePath);
|
|
194
|
-
|
|
195
|
-
if (isEntryPoint(dirent.name)) {
|
|
196
|
-
entryPoints.push(relFilePath);
|
|
197
|
-
}
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -240,21 +241,177 @@ function refineNodeProjectType(pkgPath) {
|
|
|
240
241
|
...pkg.peerDependencies,
|
|
241
242
|
};
|
|
242
243
|
|
|
243
|
-
if (deps['next'])
|
|
244
|
-
if (deps['
|
|
245
|
-
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';
|
|
246
248
|
if (deps['@angular/core']) return 'Angular';
|
|
247
|
-
if (deps['svelte'])
|
|
248
|
-
if (deps['express'])
|
|
249
|
-
if (deps['fastify'])
|
|
250
|
-
if (deps['koa'])
|
|
251
|
-
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';
|
|
252
254
|
} catch {
|
|
253
255
|
// Malformed package.json — fall through.
|
|
254
256
|
}
|
|
255
257
|
return 'Node.js / JavaScript';
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Entry Point Heuristics
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
const FALSE_POSITIVES = [
|
|
265
|
+
'/internal/', '/renderer/', '/renderers/', '/dist/', '/build/', '/generated/', '/node_modules/'
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
function isFalsePositive(relPath) {
|
|
269
|
+
const normalized = '/' + relPath + '/'; // relPath is already POSIX
|
|
270
|
+
if (FALSE_POSITIVES.some(fp => normalized.includes(fp))) return true;
|
|
271
|
+
if (relPath.includes('.test.') || relPath.includes('.spec.')) return true;
|
|
272
|
+
// Exclude config files usually not entry points
|
|
273
|
+
if (relPath.endsWith('.config.js') || relPath.endsWith('.config.ts')) return true;
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function findEntryPoints(projectType, flatFiles, rootPath) {
|
|
278
|
+
let entries = [];
|
|
279
|
+
const validFiles = flatFiles.filter(f => !isFalsePositive(f));
|
|
280
|
+
const validSet = new Set(validFiles);
|
|
281
|
+
|
|
282
|
+
if (projectType === 'Node.js / JavaScript' || projectType.startsWith('Node.js')) {
|
|
283
|
+
try {
|
|
284
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
285
|
+
if (fs.existsSync(pkgPath)) {
|
|
286
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
287
|
+
if (pkg.bin) {
|
|
288
|
+
if (typeof pkg.bin === 'string') entries.push(pkg.bin);
|
|
289
|
+
else Object.values(pkg.bin).forEach(b => entries.push(b));
|
|
290
|
+
}
|
|
291
|
+
if (pkg.main) entries.push(pkg.main);
|
|
292
|
+
}
|
|
293
|
+
} catch {}
|
|
294
|
+
|
|
295
|
+
// Normalize and filter package.json entries to ensure they exist
|
|
296
|
+
entries = entries.map(e => toPosix(e).replace(/^\.\//, '')).filter(e => validSet.has(e));
|
|
297
|
+
|
|
298
|
+
if (entries.length === 0) {
|
|
299
|
+
const fallbacks = ['src/index.ts', 'src/index.js', 'lib/index.js', 'index.js'];
|
|
300
|
+
for (const f of fallbacks) {
|
|
301
|
+
if (validSet.has(f)) { entries.push(f); break; }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
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
|
+
];
|
|
316
|
+
for (const p of priorities) {
|
|
317
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
318
|
+
}
|
|
319
|
+
} else if (projectType.includes('Java')) {
|
|
320
|
+
const applicationJava = validFiles.filter(f => f.endsWith('Application.java'));
|
|
321
|
+
if (applicationJava.length > 0) {
|
|
322
|
+
entries.push(...applicationJava);
|
|
323
|
+
} else {
|
|
324
|
+
for (const f of validFiles) {
|
|
325
|
+
if (f.endsWith('.java')) {
|
|
326
|
+
try {
|
|
327
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
328
|
+
if (content.includes('public static void main')) {
|
|
329
|
+
entries.push(f);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (projectType.includes('Python')) {
|
|
337
|
+
const priorities = ['main.py', 'app.py', '__main__.py', 'manage.py', 'run.py'];
|
|
338
|
+
for (const p of priorities) {
|
|
339
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
340
|
+
}
|
|
341
|
+
if (entries.length === 0) {
|
|
342
|
+
for (const f of validFiles) {
|
|
343
|
+
if (f.endsWith('.py')) {
|
|
344
|
+
try {
|
|
345
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
346
|
+
if (content.includes('if __name__ == "__main__":') || content.includes("if __name__ == '__main__':")) {
|
|
347
|
+
entries.push(f);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
} catch {}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
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
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (entries.length === 0) {
|
|
382
|
+
// Fallbacks for Unknown or unrecognised projects.
|
|
383
|
+
// First try well-known index file patterns.
|
|
384
|
+
for (const f of validFiles) {
|
|
385
|
+
if (/^(src\/)?index\.[a-z]+$/.test(f)) {
|
|
386
|
+
entries.push(f);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// If still empty, pick the file with the highest import/export density.
|
|
391
|
+
if (entries.length === 0) {
|
|
392
|
+
let best = null;
|
|
393
|
+
let maxScore = -1;
|
|
394
|
+
for (const f of validFiles) {
|
|
395
|
+
if (f.match(/\.(js|ts|jsx|tsx|py|java|go|rs|rb|php)$/)) {
|
|
396
|
+
try {
|
|
397
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
398
|
+
const score = (content.match(/import /g) || []).length +
|
|
399
|
+
(content.match(/export /g) || []).length +
|
|
400
|
+
(content.match(/require\(/g) || []).length;
|
|
401
|
+
if (score > maxScore) {
|
|
402
|
+
maxScore = score;
|
|
403
|
+
best = f;
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (best) entries.push(best);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return Array.from(new Set(entries));
|
|
413
|
+
}
|
|
414
|
+
|
|
258
415
|
// ---------------------------------------------------------------------------
|
|
259
416
|
// Tree helpers
|
|
260
417
|
// ---------------------------------------------------------------------------
|
|
@@ -279,8 +436,10 @@ function countFolders(node) {
|
|
|
279
436
|
// Public API
|
|
280
437
|
// ---------------------------------------------------------------------------
|
|
281
438
|
|
|
282
|
-
export function scan(targetPath) {
|
|
439
|
+
export function scan(targetPath, options = {}) {
|
|
283
440
|
const rootPath = path.resolve(targetPath);
|
|
441
|
+
const includeHidden = !!options.includeHidden;
|
|
442
|
+
const maxFiles = options.maxFiles || 50000; // Default cap of 50k files for huge repos
|
|
284
443
|
|
|
285
444
|
// Validate target
|
|
286
445
|
let stat;
|
|
@@ -293,7 +452,7 @@ export function scan(targetPath) {
|
|
|
293
452
|
/** @type {Array<string>} */
|
|
294
453
|
const flatFiles = [];
|
|
295
454
|
/** @type {Array<string>} */
|
|
296
|
-
|
|
455
|
+
let entryPoints = [];
|
|
297
456
|
|
|
298
457
|
const startTime = performance.now();
|
|
299
458
|
let tree;
|
|
@@ -301,12 +460,13 @@ export function scan(targetPath) {
|
|
|
301
460
|
let totalFolders = 0;
|
|
302
461
|
|
|
303
462
|
if (stat.isDirectory()) {
|
|
304
|
-
tree = walkDirectory(rootPath, rootPath, flatFiles,
|
|
463
|
+
tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden, maxFiles);
|
|
305
464
|
projectType = detectProjectType(rootPath);
|
|
306
465
|
// Count all directory nodes in the tree (excluding root itself).
|
|
307
466
|
totalFolders = countFolders(tree) - 1;
|
|
467
|
+
entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
|
|
308
468
|
} else if (stat.isFile()) {
|
|
309
|
-
const relFilePath = path.basename(rootPath);
|
|
469
|
+
const relFilePath = toPosix(path.basename(rootPath));
|
|
310
470
|
tree = {
|
|
311
471
|
type: 'directory',
|
|
312
472
|
name: path.basename(path.dirname(rootPath)),
|
|
@@ -320,9 +480,7 @@ export function scan(targetPath) {
|
|
|
320
480
|
}]
|
|
321
481
|
};
|
|
322
482
|
flatFiles.push(relFilePath);
|
|
323
|
-
|
|
324
|
-
entryPoints.push(relFilePath);
|
|
325
|
-
}
|
|
483
|
+
entryPoints = [relFilePath]; // A single file is its own entry point
|
|
326
484
|
} else {
|
|
327
485
|
throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
|
|
328
486
|
}
|
|
@@ -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
|
-
}
|