@lakindu_perera/toren 1.0.2 → 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 +147 -91
- package/bin/toren.js +85 -29
- package/package.json +5 -6
- package/src/scanner/scan.js +74 -23
- package/src/renderers/tree-renderer.js +0 -67
package/README.md
CHANGED
|
@@ -1,164 +1,220 @@
|
|
|
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.
|
|
44
|
+
## CLI Usage
|
|
55
45
|
|
|
56
|
-
|
|
46
|
+
Toren is designed to be simple and intuitive. Point it at any directory to generate an immediate intelligence report.
|
|
57
47
|
|
|
58
48
|
```bash
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
# Scan a specific path
|
|
50
|
+
toren /path/to/project
|
|
61
51
|
|
|
62
|
-
Scan
|
|
52
|
+
# Scan the current directory
|
|
53
|
+
toren .
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
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
|
+
# Lifecycle & Help Commands
|
|
66
|
+
toren --help
|
|
67
|
+
toren --version
|
|
68
|
+
toren --doctor
|
|
69
|
+
toren --uninstall
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
### All Flags
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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. |
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
> **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
|
|
75
86
|
|
|
76
87
|
---
|
|
77
88
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
Toren is equipped with highly accurate **framework detection** for modern development stacks. It automatically detects:
|
|
89
|
+
## Output Examples
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **Node.js** (including Express, Fastify, Koa, and TypeScript variants)
|
|
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**
|
|
91
|
+
### Console Output Example
|
|
92
|
+
The default `console` format renders a beautiful summary directly in your terminal:
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Example Output
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
Toren v1.0.1 — Codebase Onboarding Intelligence
|
|
94
|
+
```text
|
|
95
|
+
Toren v1.0.3 — Codebase Onboarding Intelligence
|
|
104
96
|
|
|
105
97
|
🔍 Project Summary
|
|
106
98
|
────────────────────────────────────────────────────────────────────────────────
|
|
107
|
-
Path: ./my-app
|
|
108
|
-
Project type:
|
|
109
|
-
Total files:
|
|
110
|
-
Total folders:
|
|
111
|
-
Scan duration:
|
|
99
|
+
Path: ./my-react-app
|
|
100
|
+
Project type: React
|
|
101
|
+
Total files: 32
|
|
102
|
+
Total folders: 6
|
|
103
|
+
Scan duration: 4 ms
|
|
112
104
|
|
|
113
105
|
🚪 Entry Points
|
|
114
106
|
────────────────────────────────────────────────────────────────────────────────
|
|
115
|
-
→ src/
|
|
116
|
-
→ src/app/layout.tsx
|
|
107
|
+
→ src/main.tsx
|
|
117
108
|
|
|
118
109
|
📁 Folder Structure (first 20 files)
|
|
119
110
|
────────────────────────────────────────────────────────────────────────────────
|
|
120
|
-
my-app/
|
|
111
|
+
my-react-app/
|
|
121
112
|
├── public/
|
|
122
|
-
│ └──
|
|
113
|
+
│ └── vite.svg
|
|
123
114
|
├── src/
|
|
124
|
-
│ ├──
|
|
125
|
-
│ │
|
|
126
|
-
│
|
|
127
|
-
│ ├──
|
|
128
|
-
│
|
|
129
|
-
│
|
|
130
|
-
|
|
131
|
-
│ └── utils.ts
|
|
132
|
-
├── .eslintrc.json
|
|
133
|
-
├── next.config.js
|
|
115
|
+
│ ├── assets/
|
|
116
|
+
│ │ └── react.svg
|
|
117
|
+
│ ├── App.css
|
|
118
|
+
│ ├── App.tsx
|
|
119
|
+
│ ├── index.css
|
|
120
|
+
│ └── main.tsx
|
|
121
|
+
├── index.html
|
|
134
122
|
├── package.json
|
|
135
|
-
└──
|
|
136
|
-
… and
|
|
123
|
+
└── vite.config.ts
|
|
124
|
+
… and 21 more file(s) not shown
|
|
137
125
|
|
|
138
126
|
────────────────────────────────────────────────────────────────────────────────
|
|
139
127
|
✅ Scan complete.
|
|
140
128
|
```
|
|
141
129
|
|
|
130
|
+
### JSON Output Example
|
|
131
|
+
Generate machine-readable output for scripts, toolchains, or AI context windows by running `toren --format json`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"project": {
|
|
136
|
+
"path": "./my-react-app",
|
|
137
|
+
"type": "React",
|
|
138
|
+
"framework": "React"
|
|
139
|
+
},
|
|
140
|
+
"summary": {
|
|
141
|
+
"totalFiles": 32,
|
|
142
|
+
"totalFolders": 6,
|
|
143
|
+
"scanDurationMs": 4
|
|
144
|
+
},
|
|
145
|
+
"entryPoints": [
|
|
146
|
+
"src/main.tsx"
|
|
147
|
+
],
|
|
148
|
+
"structure": [
|
|
149
|
+
{
|
|
150
|
+
"type": "folder",
|
|
151
|
+
"name": "src",
|
|
152
|
+
"children": [
|
|
153
|
+
{ "type": "file", "name": "App.tsx" },
|
|
154
|
+
{ "type": "file", "name": "main.tsx" }
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{ "type": "file", "name": "package.json" }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
*Note: You can also generate rich, GitHub-flavored Markdown (`--format markdown`) or self-contained HTML reports (`--format html`) for documentation purposes!*
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Architecture
|
|
167
|
+
|
|
168
|
+
Toren's internal architecture emphasizes modular design, separation of concerns, and a strict **zero dependency** philosophy.
|
|
169
|
+
|
|
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.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Why Toren Exists
|
|
178
|
+
|
|
179
|
+
Modern software development moves fast, but **onboarding into large codebases is slow**.
|
|
180
|
+
|
|
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.
|
|
182
|
+
|
|
183
|
+
**Toren solves the "first 5 minutes of any repo" problem.**
|
|
184
|
+
|
|
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.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Roadmap (Future Improvements)
|
|
190
|
+
|
|
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
|
|
195
|
+
|
|
142
196
|
---
|
|
143
197
|
|
|
144
198
|
## Contributing
|
|
145
199
|
|
|
146
|
-
Contributions to improve this
|
|
200
|
+
Contributions to improve this codebase analyzer CLI are always welcome!
|
|
147
201
|
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
151
|
-
4.
|
|
152
|
-
5.
|
|
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
|
|
153
207
|
|
|
154
|
-
Please ensure
|
|
208
|
+
*Please ensure all new features maintain the strict zero-dependency architecture rule.*
|
|
155
209
|
|
|
156
210
|
---
|
|
157
211
|
|
|
158
|
-
## License
|
|
212
|
+
## 📄 License
|
|
159
213
|
|
|
160
|
-
|
|
214
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
161
215
|
|
|
162
216
|
---
|
|
163
217
|
|
|
164
|
-
|
|
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,9 +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
|
|
74
81
|
* @property {boolean} [includeHidden] - Whether to include hidden files
|
|
82
|
+
* @property {number} [maxFiles] - Max files to scan
|
|
75
83
|
*/
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -89,18 +97,18 @@ function parseArgs() {
|
|
|
89
97
|
// ── Informational flags ─────────────────────────────────────────────────
|
|
90
98
|
if (args.includes('--help') || args.includes('-h')) {
|
|
91
99
|
printHelp();
|
|
92
|
-
return { action: 'exit' };
|
|
100
|
+
return { action: 'exit', code: 0 };
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
if (args.includes('--version') || args.includes('-
|
|
103
|
+
if (args.includes('--version') || args.includes('-V') || args.includes('-v')) {
|
|
96
104
|
console.log(pkg.version);
|
|
97
|
-
return { action: 'exit' };
|
|
105
|
+
return { action: 'exit', code: 0 };
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
// ── Lifecycle commands ──────────────────────────────────────────────────
|
|
101
109
|
if (args.includes('--doctor')) {
|
|
102
110
|
runDoctor(pkg.version);
|
|
103
|
-
return { action: 'exit' };
|
|
111
|
+
return { action: 'exit', code: 0 };
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
if (args.includes('--uninstall')) {
|
|
@@ -115,9 +123,19 @@ function parseArgs() {
|
|
|
115
123
|
|
|
116
124
|
const formatIdx = args.indexOf('--format');
|
|
117
125
|
if (formatIdx !== -1) {
|
|
118
|
-
|
|
119
|
-
// If the
|
|
120
|
-
|
|
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;
|
|
121
139
|
} else if (args.includes('--json')) {
|
|
122
140
|
format = 'json';
|
|
123
141
|
}
|
|
@@ -125,30 +143,62 @@ function parseArgs() {
|
|
|
125
143
|
// ── Hidden files ────────────────────────────────────────────────────────
|
|
126
144
|
const includeHidden = args.includes('--include-hidden');
|
|
127
145
|
|
|
128
|
-
|
|
129
146
|
// ── Target path ─────────────────────────────────────────────────────────
|
|
130
147
|
// Build the set of tokens that are consumed as values by named flags so
|
|
131
148
|
// we don't accidentally treat them as the positional path argument.
|
|
132
|
-
// Currently only --format consumes a value token.
|
|
133
149
|
const consumedValues = new Set();
|
|
134
|
-
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined) {
|
|
150
|
+
if (formatIdx !== -1 && args[formatIdx + 1] !== undefined && !args[formatIdx + 1].startsWith('-')) {
|
|
135
151
|
consumedValues.add(args[formatIdx + 1]);
|
|
136
152
|
}
|
|
137
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
|
+
|
|
138
176
|
// First non-flag, non-consumed token is the target path; default to cwd.
|
|
139
177
|
const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
|
|
140
178
|
|
|
141
179
|
// ── Unknown flag check ──────────────────────────────────────────────────
|
|
142
|
-
const knownFlags = new Set([
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
|
|
145
195
|
if (unknownFlag) {
|
|
146
196
|
console.error(`\x1b[31m Unknown flag: ${unknownFlag}\x1b[0m`);
|
|
147
197
|
console.error(` Run \x1b[36mtoren --help\x1b[0m for usage.\n`);
|
|
148
|
-
return { action: 'exit' };
|
|
198
|
+
return { action: 'exit', code: 1 };
|
|
149
199
|
}
|
|
150
200
|
|
|
151
|
-
return { action: 'scan', target, format, includeHidden };
|
|
201
|
+
return { action: 'scan', target, format, includeHidden, maxFiles };
|
|
152
202
|
}
|
|
153
203
|
|
|
154
204
|
// ---------------------------------------------------------------------------
|
|
@@ -183,7 +233,10 @@ function assertValidFormat(format) {
|
|
|
183
233
|
|
|
184
234
|
(function main() {
|
|
185
235
|
const parsed = parseArgs();
|
|
186
|
-
|
|
236
|
+
|
|
237
|
+
if (parsed.action === 'exit') {
|
|
238
|
+
process.exit(parsed.code ?? 0);
|
|
239
|
+
}
|
|
187
240
|
if (parsed.action === 'wait') return;
|
|
188
241
|
|
|
189
242
|
// Validate before scanning — fail fast on bad format names.
|
|
@@ -192,7 +245,10 @@ function assertValidFormat(format) {
|
|
|
192
245
|
const render = renderers[parsed.format];
|
|
193
246
|
|
|
194
247
|
try {
|
|
195
|
-
const result = scan(parsed.target, {
|
|
248
|
+
const result = scan(parsed.target, {
|
|
249
|
+
includeHidden: parsed.includeHidden,
|
|
250
|
+
maxFiles: parsed.maxFiles,
|
|
251
|
+
});
|
|
196
252
|
render(result, { cwd: process.cwd() });
|
|
197
253
|
} catch (err) {
|
|
198
254
|
// Render errors in the requested format where possible.
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakindu_perera/toren",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
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
|
-
}
|