@lakindu_perera/toren 1.0.0 → 1.0.2
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 +57 -247
- package/bin/toren.js +8 -3
- package/package.json +14 -9
- package/src/renderers/console-renderer.js +18 -6
- package/src/scanner/scan.js +151 -44
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Toren
|
|
1
|
+
# Toren — Codebase Intelligence CLI
|
|
2
2
|
|
|
3
3
|
> Understand any codebase in seconds.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/toren)
|
|
5
|
+
[](https://www.npmjs.com/package/@lakindu_perera/toren)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](package.json)
|
|
@@ -11,85 +11,96 @@
|
|
|
11
11
|
|
|
12
12
|
## What is Toren?
|
|
13
13
|
|
|
14
|
-
**Toren** is a
|
|
14
|
+
**Toren** is a powerful **codebase intelligence CLI** and **project analysis** tool designed to help developers understand unfamiliar repositories instantly. By running this lightweight **scanner**, you gain immediate insight into the architecture of any software project. It performs robust **framework detection**, identifies critical application start files through **entry point analysis**, and generates a clear **project structure visualization**—all without requiring you to read a single line of code.
|
|
15
15
|
|
|
16
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
Whether you've just cloned an open-source project, joined a new team, or are reviewing a client's codebase, Toren cuts through the noise and gets you oriented fast.
|
|
16
|
+
Whether you are onboarding to a new team, auditing a complex repository, or looking for a fast **monorepo analyzer**, Toren cuts through the noise and gets you oriented fast.
|
|
24
17
|
|
|
25
18
|
---
|
|
26
19
|
|
|
27
20
|
## Features
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
22
|
+
Toren is built to deliver comprehensive repository insights efficiently:
|
|
23
|
+
|
|
24
|
+
- **Codebase Scanning**: Recursively walks any project in milliseconds to gather deep architectural insights.
|
|
25
|
+
- **Framework Detection**: Instantly identifies the underlying technologies powering the application.
|
|
26
|
+
- **Entry Point Analysis**: Automatically pinpoints where execution begins (e.g., `index.js`, `main.ts`, `App.tsx`, `Application.java`).
|
|
27
|
+
- **Project Structure Visualization**: Generates a beautiful, hierarchical directory tree right in your terminal.
|
|
28
|
+
- **Monorepo Support**: Gracefully handles complex, multi-package repositories without failing.
|
|
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.
|
|
38
31
|
|
|
39
32
|
---
|
|
40
33
|
|
|
41
34
|
## Installation
|
|
42
35
|
|
|
43
|
-
Install globally
|
|
36
|
+
Install the **Toren CLI tool** globally via npm to use it across all your local projects:
|
|
44
37
|
|
|
45
38
|
```bash
|
|
46
|
-
npm install -g toren
|
|
39
|
+
npm install -g @lakindu_perera/toren
|
|
47
40
|
```
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
Alternatively, you can run it instantly without global installation:
|
|
50
43
|
|
|
51
44
|
```bash
|
|
52
|
-
npx toren
|
|
45
|
+
npx @lakindu_perera/toren
|
|
53
46
|
```
|
|
54
47
|
|
|
55
48
|
**Requirements:** Node.js 18.0.0 or higher.
|
|
56
49
|
|
|
57
50
|
---
|
|
58
51
|
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
Scan the current directory:
|
|
52
|
+
## Usage Examples
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
toren
|
|
65
|
-
```
|
|
54
|
+
Navigate to your target project folder or point Toren directly to a repository.
|
|
66
55
|
|
|
67
|
-
Scan
|
|
56
|
+
Scan the current directory for an instant summary:
|
|
68
57
|
|
|
69
58
|
```bash
|
|
70
59
|
toren .
|
|
71
|
-
toren ../my-project
|
|
72
|
-
toren /path/to/any/repo
|
|
73
60
|
```
|
|
74
61
|
|
|
75
|
-
|
|
62
|
+
Scan and output the analysis in JSON format (ideal for toolchain integrations):
|
|
76
63
|
|
|
77
64
|
```bash
|
|
78
|
-
toren --json
|
|
65
|
+
toren . --format json
|
|
79
66
|
```
|
|
80
67
|
|
|
81
|
-
|
|
68
|
+
Run a deep analysis on the codebase:
|
|
82
69
|
|
|
83
70
|
```bash
|
|
84
|
-
toren --
|
|
71
|
+
toren . --analyze
|
|
85
72
|
```
|
|
86
73
|
|
|
74
|
+
*Note: You can also point Toren to any absolute or relative path, e.g., `toren ../my-project`.*
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Supported Frameworks
|
|
79
|
+
|
|
80
|
+
Toren is equipped with highly accurate **framework detection** for modern development stacks. It automatically detects:
|
|
81
|
+
|
|
82
|
+
- **Next.js**
|
|
83
|
+
- **React**
|
|
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**
|
|
95
|
+
|
|
96
|
+
If a specific marker is not found, Toren intelligently falls back to structural heuristics to identify generic entry points, ensuring you always get meaningful project analysis.
|
|
97
|
+
|
|
87
98
|
---
|
|
88
99
|
|
|
89
100
|
## Example Output
|
|
90
101
|
|
|
91
102
|
```
|
|
92
|
-
Toren v1.0.
|
|
103
|
+
Toren v1.0.1 — Codebase Onboarding Intelligence
|
|
93
104
|
|
|
94
105
|
🔍 Project Summary
|
|
95
106
|
────────────────────────────────────────────────────────────────────────────────
|
|
@@ -130,218 +141,17 @@ my-app/
|
|
|
130
141
|
|
|
131
142
|
---
|
|
132
143
|
|
|
133
|
-
## JSON Output
|
|
134
|
-
|
|
135
|
-
Use `toren --json` to get a machine-readable result suitable for piping into other tools:
|
|
136
|
-
|
|
137
|
-
```json
|
|
138
|
-
{
|
|
139
|
-
"project": {
|
|
140
|
-
"path": "./my-app",
|
|
141
|
-
"type": "Next.js",
|
|
142
|
-
"framework": "Next.js"
|
|
143
|
-
},
|
|
144
|
-
"summary": {
|
|
145
|
-
"totalFiles": 48,
|
|
146
|
-
"totalFolders": 11,
|
|
147
|
-
"scanDurationMs": 3
|
|
148
|
-
},
|
|
149
|
-
"entryPoints": [
|
|
150
|
-
"src/app/page.tsx",
|
|
151
|
-
"src/app/layout.tsx"
|
|
152
|
-
],
|
|
153
|
-
"structure": [
|
|
154
|
-
{
|
|
155
|
-
"type": "folder",
|
|
156
|
-
"name": "src",
|
|
157
|
-
"children": [
|
|
158
|
-
{
|
|
159
|
-
"type": "folder",
|
|
160
|
-
"name": "app",
|
|
161
|
-
"children": [
|
|
162
|
-
{ "type": "file", "name": "layout.tsx" },
|
|
163
|
-
{ "type": "file", "name": "page.tsx" }
|
|
164
|
-
]
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
},
|
|
168
|
-
{ "type": "file", "name": "package.json" },
|
|
169
|
-
{ "type": "file", "name": "next.config.js" }
|
|
170
|
-
]
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
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.
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## CLI Reference
|
|
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
|
-
---
|
|
220
|
-
|
|
221
|
-
## Project Structure
|
|
222
|
-
|
|
223
|
-
```
|
|
224
|
-
toren/
|
|
225
|
-
├── bin/
|
|
226
|
-
│ └── toren.js # CLI entry point — argument parsing and orchestration
|
|
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
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## How It Works
|
|
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.).
|
|
245
|
-
|
|
246
|
-
The result is an in-memory tree of `DirNode` and `FileNode` objects, along with a flat list of all relative file paths.
|
|
247
|
-
|
|
248
|
-
### 2. Framework Detection
|
|
249
|
-
|
|
250
|
-
After walking the directory, the scanner checks for known marker files at the project root (e.g. `package.json`, `go.mod`, `Cargo.toml`). For `package.json`, it reads the file and inspects `dependencies`, `devDependencies`, and `peerDependencies` to determine the specific framework (React, Next.js, Vue, Angular, etc.).
|
|
251
|
-
|
|
252
|
-
### 3. Entry Point Detection
|
|
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.
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Architecture
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
CLI (bin/toren.js)
|
|
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 md`)
|
|
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
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
144
|
## Contributing
|
|
329
145
|
|
|
330
|
-
Contributions
|
|
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
|
|
146
|
+
Contributions to improve this **repository analyzer** and **developer tool** are welcome!
|
|
337
147
|
|
|
338
|
-
|
|
148
|
+
1. **Fork** this repository.
|
|
149
|
+
2. **Create** a feature branch: `git checkout -b feature/my-feature`.
|
|
150
|
+
3. **Commit** your changes: `git commit -m "feat: add my feature"`.
|
|
151
|
+
4. **Push** to your branch: `git push origin feature/my-feature`.
|
|
152
|
+
5. **Open** a Pull Request.
|
|
339
153
|
|
|
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
|
|
154
|
+
Please ensure any additions maintain the zero-dependency architecture.
|
|
345
155
|
|
|
346
156
|
---
|
|
347
157
|
|
|
@@ -351,4 +161,4 @@ For bugs or feature requests, please [open an issue](https://github.com/your-use
|
|
|
351
161
|
|
|
352
162
|
---
|
|
353
163
|
|
|
354
|
-
*If Toren saves you time, consider starring the repository — it helps
|
|
164
|
+
*If Toren saves you time during your codebase analysis, please consider starring the repository — it helps other developers discover this project.* ⭐
|
package/bin/toren.js
CHANGED
|
@@ -71,6 +71,7 @@ ${formatList}
|
|
|
71
71
|
* @property {'exit'|'wait'|'scan'} action
|
|
72
72
|
* @property {string} [target] - Resolved path to scan
|
|
73
73
|
* @property {string} [format] - Renderer format name
|
|
74
|
+
* @property {boolean} [includeHidden] - Whether to include hidden files
|
|
74
75
|
*/
|
|
75
76
|
|
|
76
77
|
/**
|
|
@@ -121,6 +122,10 @@ function parseArgs() {
|
|
|
121
122
|
format = 'json';
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// ── Hidden files ────────────────────────────────────────────────────────
|
|
126
|
+
const includeHidden = args.includes('--include-hidden');
|
|
127
|
+
|
|
128
|
+
|
|
124
129
|
// ── Target path ─────────────────────────────────────────────────────────
|
|
125
130
|
// Build the set of tokens that are consumed as values by named flags so
|
|
126
131
|
// we don't accidentally treat them as the positional path argument.
|
|
@@ -134,7 +139,7 @@ function parseArgs() {
|
|
|
134
139
|
const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
|
|
135
140
|
|
|
136
141
|
// ── Unknown flag check ──────────────────────────────────────────────────
|
|
137
|
-
const knownFlags = new Set(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json']);
|
|
142
|
+
const knownFlags = new Set(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json', '--include-hidden']);
|
|
138
143
|
const unknownFlag = args.find(a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a));
|
|
139
144
|
|
|
140
145
|
if (unknownFlag) {
|
|
@@ -143,7 +148,7 @@ function parseArgs() {
|
|
|
143
148
|
return { action: 'exit' };
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
return { action: 'scan', target, format };
|
|
151
|
+
return { action: 'scan', target, format, includeHidden };
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
// ---------------------------------------------------------------------------
|
|
@@ -187,7 +192,7 @@ function assertValidFormat(format) {
|
|
|
187
192
|
const render = renderers[parsed.format];
|
|
188
193
|
|
|
189
194
|
try {
|
|
190
|
-
const result = scan(parsed.target);
|
|
195
|
+
const result = scan(parsed.target, { includeHidden: parsed.includeHidden });
|
|
191
196
|
render(result, { cwd: process.cwd() });
|
|
192
197
|
} catch (err) {
|
|
193
198
|
// Render errors in the requested format where possible.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakindu_perera/toren",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A CLI
|
|
3
|
+
"version": "1.0.2",
|
|
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
9
|
"preferGlobal": true,
|
|
10
10
|
"main": "bin/toren.js",
|
|
@@ -17,16 +17,21 @@
|
|
|
17
17
|
"lint": "node --check bin/toren.js src/scanner/scan.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
|
+
"typescript cli",
|
|
29
|
+
"onboarding"
|
|
25
30
|
],
|
|
26
31
|
"author": "Lakindu Perera",
|
|
27
32
|
"repository": {
|
|
28
33
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/lakindudev/toren.git"
|
|
34
|
+
"url": "git+https://github.com/lakindudev/toren.git"
|
|
30
35
|
},
|
|
31
36
|
"bugs": {
|
|
32
37
|
"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:
|
|
@@ -65,24 +66,6 @@ const PROJECT_TYPE_MARKERS = [
|
|
|
65
66
|
{ marker: 'mix.exs', label: 'Elixir' },
|
|
66
67
|
];
|
|
67
68
|
|
|
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
69
|
// ---------------------------------------------------------------------------
|
|
87
70
|
// Types (JSDoc — no TypeScript dependency required)
|
|
88
71
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +75,7 @@ function isEntryPoint(filename) {
|
|
|
92
75
|
* @property {'file'} type
|
|
93
76
|
* @property {string} name - Basename of the file
|
|
94
77
|
* @property {string} fullPath - Absolute path
|
|
95
|
-
* @property {string} relPath - Path relative to the scanned root
|
|
78
|
+
* @property {string} relPath - Path relative to the scanned root (POSIX style)
|
|
96
79
|
*/
|
|
97
80
|
|
|
98
81
|
/**
|
|
@@ -100,7 +83,7 @@ function isEntryPoint(filename) {
|
|
|
100
83
|
* @property {'directory'} type
|
|
101
84
|
* @property {string} name - Basename of the directory
|
|
102
85
|
* @property {string} fullPath - Absolute path
|
|
103
|
-
* @property {string} relPath - Path relative to the scanned root
|
|
86
|
+
* @property {string} relPath - Path relative to the scanned root (POSIX style)
|
|
104
87
|
* @property {Array<FileNode|DirNode>} children
|
|
105
88
|
*/
|
|
106
89
|
|
|
@@ -119,31 +102,40 @@ function isEntryPoint(filename) {
|
|
|
119
102
|
// Internal helpers
|
|
120
103
|
// ---------------------------------------------------------------------------
|
|
121
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Normalize a path to use POSIX separators ('/').
|
|
107
|
+
*/
|
|
108
|
+
function toPosix(p) {
|
|
109
|
+
return p.replace(/\\/g, '/');
|
|
110
|
+
}
|
|
111
|
+
|
|
122
112
|
/**
|
|
123
113
|
* Determine whether a directory entry should be skipped.
|
|
124
114
|
*
|
|
125
115
|
* @param {string} name - Basename of the entry
|
|
126
116
|
* @param {fs.Dirent} dirent
|
|
117
|
+
* @param {boolean} includeHidden
|
|
127
118
|
* @returns {boolean}
|
|
128
119
|
*/
|
|
129
|
-
function shouldIgnore(name, dirent) {
|
|
130
|
-
if (name.startsWith('.')
|
|
120
|
+
function shouldIgnore(name, dirent, includeHidden) {
|
|
121
|
+
if (!includeHidden && name.startsWith('.')) return true;
|
|
131
122
|
return IGNORED_DIRS.has(name);
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
/**
|
|
135
126
|
* Recursively walk `dirPath`, building a DirNode tree.
|
|
136
|
-
* Also populates `flatFiles`
|
|
127
|
+
* Also populates `flatFiles` array by reference.
|
|
137
128
|
*
|
|
138
129
|
* @param {string} dirPath - Absolute path of the current directory
|
|
139
130
|
* @param {string} rootPath - Absolute path of the scan root (for relative paths)
|
|
140
|
-
* @param {Array<string>} flatFiles
|
|
141
|
-
* @param {
|
|
131
|
+
* @param {Array<string>} flatFiles - Accumulator for all relative file paths
|
|
132
|
+
* @param {boolean} includeHidden - Whether to include hidden files
|
|
142
133
|
* @returns {DirNode}
|
|
143
134
|
*/
|
|
144
|
-
function walkDirectory(dirPath, rootPath, flatFiles,
|
|
135
|
+
function walkDirectory(dirPath, rootPath, flatFiles, includeHidden) {
|
|
145
136
|
const name = path.basename(dirPath);
|
|
146
|
-
|
|
137
|
+
let rawRelPath = path.relative(rootPath, dirPath) || '.';
|
|
138
|
+
const relPath = toPosix(rawRelPath);
|
|
147
139
|
|
|
148
140
|
/** @type {DirNode} */
|
|
149
141
|
const node = {
|
|
@@ -157,8 +149,9 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
157
149
|
let entries;
|
|
158
150
|
try {
|
|
159
151
|
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
160
|
-
} catch {
|
|
161
|
-
// Permission-denied or unreadable directory — skip
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// Permission-denied or unreadable directory — skip and log warnings instead of failing.
|
|
154
|
+
console.warn(`[warn] Skipping ${dirPath} (permission denied)`);
|
|
162
155
|
return node;
|
|
163
156
|
}
|
|
164
157
|
|
|
@@ -171,15 +164,15 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
171
164
|
});
|
|
172
165
|
|
|
173
166
|
for (const dirent of entries) {
|
|
174
|
-
if (shouldIgnore(dirent.name, dirent)) continue;
|
|
167
|
+
if (shouldIgnore(dirent.name, dirent, includeHidden)) continue;
|
|
175
168
|
|
|
176
169
|
const childPath = path.join(dirPath, dirent.name);
|
|
177
170
|
|
|
178
171
|
if (dirent.isDirectory()) {
|
|
179
|
-
const childNode = walkDirectory(childPath, rootPath, flatFiles,
|
|
172
|
+
const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden);
|
|
180
173
|
node.children.push(childNode);
|
|
181
174
|
} else if (dirent.isFile()) {
|
|
182
|
-
const relFilePath = path.relative(rootPath, childPath);
|
|
175
|
+
const relFilePath = toPosix(path.relative(rootPath, childPath));
|
|
183
176
|
|
|
184
177
|
/** @type {FileNode} */
|
|
185
178
|
const fileNode = {
|
|
@@ -191,10 +184,6 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
|
191
184
|
|
|
192
185
|
node.children.push(fileNode);
|
|
193
186
|
flatFiles.push(relFilePath);
|
|
194
|
-
|
|
195
|
-
if (isEntryPoint(dirent.name)) {
|
|
196
|
-
entryPoints.push(relFilePath);
|
|
197
|
-
}
|
|
198
187
|
}
|
|
199
188
|
}
|
|
200
189
|
|
|
@@ -255,6 +244,124 @@ function refineNodeProjectType(pkgPath) {
|
|
|
255
244
|
return 'Node.js / JavaScript';
|
|
256
245
|
}
|
|
257
246
|
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Entry Point Heuristics
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
const FALSE_POSITIVES = [
|
|
252
|
+
'/internal/', '/renderer/', '/renderers/', '/dist/', '/build/', '/generated/', '/node_modules/'
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
function isFalsePositive(relPath) {
|
|
256
|
+
const normalized = '/' + relPath + '/'; // relPath is already POSIX
|
|
257
|
+
if (FALSE_POSITIVES.some(fp => normalized.includes(fp))) return true;
|
|
258
|
+
if (relPath.includes('.test.') || relPath.includes('.spec.')) return true;
|
|
259
|
+
// Exclude config files usually not entry points
|
|
260
|
+
if (relPath.endsWith('.config.js') || relPath.endsWith('.config.ts')) return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findEntryPoints(projectType, flatFiles, rootPath) {
|
|
265
|
+
let entries = [];
|
|
266
|
+
const validFiles = flatFiles.filter(f => !isFalsePositive(f));
|
|
267
|
+
const validSet = new Set(validFiles);
|
|
268
|
+
|
|
269
|
+
if (projectType === 'Node.js / JavaScript' || projectType.startsWith('Node.js')) {
|
|
270
|
+
try {
|
|
271
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
272
|
+
if (fs.existsSync(pkgPath)) {
|
|
273
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
274
|
+
if (pkg.bin) {
|
|
275
|
+
if (typeof pkg.bin === 'string') entries.push(pkg.bin);
|
|
276
|
+
else Object.values(pkg.bin).forEach(b => entries.push(b));
|
|
277
|
+
}
|
|
278
|
+
if (pkg.main) entries.push(pkg.main);
|
|
279
|
+
}
|
|
280
|
+
} catch {}
|
|
281
|
+
|
|
282
|
+
// Normalize and filter package.json entries to ensure they exist
|
|
283
|
+
entries = entries.map(e => toPosix(e).replace(/^\.\//, '')).filter(e => validSet.has(e));
|
|
284
|
+
|
|
285
|
+
if (entries.length === 0) {
|
|
286
|
+
const fallbacks = ['src/index.ts', 'src/index.js', 'lib/index.js', 'index.js'];
|
|
287
|
+
for (const f of fallbacks) {
|
|
288
|
+
if (validSet.has(f)) { entries.push(f); break; }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else if (projectType === 'React' || projectType === 'Next.js' || projectType === 'Vue.js' || projectType === 'Angular' || projectType === 'Svelte') {
|
|
292
|
+
const priorities = ['src/main.tsx', 'src/main.jsx', 'pages/_app.tsx', 'app/layout.tsx', 'src/App.tsx', 'index.html'];
|
|
293
|
+
for (const p of priorities) {
|
|
294
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
295
|
+
}
|
|
296
|
+
} else if (projectType.includes('Java')) {
|
|
297
|
+
const applicationJava = validFiles.filter(f => f.endsWith('Application.java'));
|
|
298
|
+
if (applicationJava.length > 0) {
|
|
299
|
+
entries.push(...applicationJava);
|
|
300
|
+
} else {
|
|
301
|
+
for (const f of validFiles) {
|
|
302
|
+
if (f.endsWith('.java')) {
|
|
303
|
+
try {
|
|
304
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
305
|
+
if (content.includes('public static void main')) {
|
|
306
|
+
entries.push(f);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else if (projectType.includes('Python')) {
|
|
314
|
+
const priorities = ['main.py', 'app.py', '__main__.py'];
|
|
315
|
+
for (const p of priorities) {
|
|
316
|
+
if (validSet.has(p)) { entries.push(p); break; }
|
|
317
|
+
}
|
|
318
|
+
if (entries.length === 0) {
|
|
319
|
+
for (const f of validFiles) {
|
|
320
|
+
if (f.endsWith('.py')) {
|
|
321
|
+
try {
|
|
322
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
323
|
+
if (content.includes('if __name__ == "__main__":') || content.includes("if __name__ == '__main__':")) {
|
|
324
|
+
entries.push(f);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
} catch {}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (entries.length === 0) {
|
|
334
|
+
// Fallbacks for Unknown or missed projects
|
|
335
|
+
let best = null;
|
|
336
|
+
let maxScore = -1;
|
|
337
|
+
for (const f of validFiles) {
|
|
338
|
+
if (/^(src\/)?index\.[a-z]+$/.test(f)) {
|
|
339
|
+
entries.push(f);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (entries.length === 0) {
|
|
344
|
+
for (const f of validFiles) {
|
|
345
|
+
if (f.match(/\.(js|ts|jsx|tsx|py|java|go|rb|php)$/)) {
|
|
346
|
+
try {
|
|
347
|
+
const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
|
|
348
|
+
const score = (content.match(/import /g) || []).length +
|
|
349
|
+
(content.match(/export /g) || []).length +
|
|
350
|
+
(content.match(/require\(/g) || []).length;
|
|
351
|
+
if (score > maxScore) {
|
|
352
|
+
maxScore = score;
|
|
353
|
+
best = f;
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (best) entries.push(best);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return Array.from(new Set(entries));
|
|
363
|
+
}
|
|
364
|
+
|
|
258
365
|
// ---------------------------------------------------------------------------
|
|
259
366
|
// Tree helpers
|
|
260
367
|
// ---------------------------------------------------------------------------
|
|
@@ -279,8 +386,9 @@ function countFolders(node) {
|
|
|
279
386
|
// Public API
|
|
280
387
|
// ---------------------------------------------------------------------------
|
|
281
388
|
|
|
282
|
-
export function scan(targetPath) {
|
|
389
|
+
export function scan(targetPath, options = {}) {
|
|
283
390
|
const rootPath = path.resolve(targetPath);
|
|
391
|
+
const includeHidden = !!options.includeHidden;
|
|
284
392
|
|
|
285
393
|
// Validate target
|
|
286
394
|
let stat;
|
|
@@ -293,7 +401,7 @@ export function scan(targetPath) {
|
|
|
293
401
|
/** @type {Array<string>} */
|
|
294
402
|
const flatFiles = [];
|
|
295
403
|
/** @type {Array<string>} */
|
|
296
|
-
|
|
404
|
+
let entryPoints = [];
|
|
297
405
|
|
|
298
406
|
const startTime = performance.now();
|
|
299
407
|
let tree;
|
|
@@ -301,12 +409,13 @@ export function scan(targetPath) {
|
|
|
301
409
|
let totalFolders = 0;
|
|
302
410
|
|
|
303
411
|
if (stat.isDirectory()) {
|
|
304
|
-
tree = walkDirectory(rootPath, rootPath, flatFiles,
|
|
412
|
+
tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden);
|
|
305
413
|
projectType = detectProjectType(rootPath);
|
|
306
414
|
// Count all directory nodes in the tree (excluding root itself).
|
|
307
415
|
totalFolders = countFolders(tree) - 1;
|
|
416
|
+
entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
|
|
308
417
|
} else if (stat.isFile()) {
|
|
309
|
-
const relFilePath = path.basename(rootPath);
|
|
418
|
+
const relFilePath = toPosix(path.basename(rootPath));
|
|
310
419
|
tree = {
|
|
311
420
|
type: 'directory',
|
|
312
421
|
name: path.basename(path.dirname(rootPath)),
|
|
@@ -320,9 +429,7 @@ export function scan(targetPath) {
|
|
|
320
429
|
}]
|
|
321
430
|
};
|
|
322
431
|
flatFiles.push(relFilePath);
|
|
323
|
-
|
|
324
|
-
entryPoints.push(relFilePath);
|
|
325
|
-
}
|
|
432
|
+
entryPoints = [relFilePath]; // A single file is its own entry point
|
|
326
433
|
} else {
|
|
327
434
|
throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
|
|
328
435
|
}
|