@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 CHANGED
@@ -1,354 +1,220 @@
1
- # Toren
1
+ # Toren — Codebase Analyzer CLI
2
2
 
3
- > Understand any codebase in seconds.
3
+ > The fastest way to understand any project structure. A zero-dependency codebase scanner CLI for modern developers.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@lakindu_perera/toren.svg)](https://www.npmjs.com/package/@lakindu_perera/toren)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
- [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
6
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
8
7
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-success.svg)](package.json)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
9
 
10
10
  ---
11
11
 
12
12
  ## What is Toren?
13
13
 
14
- **Toren** is a lightweight, zero-dependency CLI tool that scans a software project and gives you an instant, structured overview without reading a single line of code.
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
- 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
+ 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
- - **Fast scanning** recursively walks a project in milliseconds
30
- - **Zero external dependencies** pure Node.js stdlib only
31
- - **Framework detection** identifies React, Next.js, Vue, Angular, Go, Rust, Python, and more
32
- - **Entry point detection** pinpoints `index.js`, `main.ts`, `App.tsx`, `Application.java`, and other common entry files
33
- - **File tree preview** visual directory structure, up to 4 levels deep
34
- - **Beautiful terminal output** ANSI-styled, readable at a glance
35
- - **JSON output** machine-readable format for scripting and tooling integration
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 with npm:
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
- ## Quick Start
44
+ ## CLI Usage
60
45
 
61
- Scan the current directory:
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
- toren
65
- ```
49
+ # Scan a specific path
50
+ toren /path/to/project
66
51
 
67
- Scan a specific path:
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
- ```bash
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
- Check your global installation:
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
- ```bash
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
- ## Example Output
89
+ ## Output Examples
90
90
 
91
- ```
92
- Toren v1.0.0 — Codebase Onboarding Intelligence
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: Next.js
98
- Total files: 48
99
- Total folders: 11
100
- Scan duration: 3 ms
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/app/page.tsx
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
- │ └── favicon.ico
113
+ │ └── vite.svg
112
114
  ├── src/
113
- │ ├── app/
114
- │ │ ├── layout.tsx
115
- │ └── page.tsx
116
- │ ├── components/
117
- ├── Header.tsx
118
- └── Footer.tsx
119
- │ └── lib/
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
- └── tsconfig.json
125
- … and 28 more file(s) not shown
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": "Next.js",
142
- "framework": "Next.js"
136
+ "path": "./my-react-app",
137
+ "type": "React",
138
+ "framework": "React"
143
139
  },
144
140
  "summary": {
145
- "totalFiles": 48,
146
- "totalFolders": 11,
147
- "scanDurationMs": 3
141
+ "totalFiles": 32,
142
+ "totalFolders": 6,
143
+ "scanDurationMs": 4
148
144
  },
149
145
  "entryPoints": [
150
- "src/app/page.tsx",
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
- "type": "folder",
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
- ## 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
- ---
166
+ ## Architecture
220
167
 
221
- ## Project Structure
168
+ Toren's internal architecture emphasizes modular design, separation of concerns, and a strict **zero dependency** philosophy.
222
169
 
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
- ```
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
- ## 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.).
177
+ ## Why Toren Exists
245
178
 
246
- The result is an in-memory tree of `DirNode` and `FileNode` objects, along with a flat list of all relative file paths.
179
+ Modern software development moves fast, but **onboarding into large codebases is slow**.
247
180
 
248
- ### 2. Framework Detection
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
- 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.).
183
+ **Toren solves the "first 5 minutes of any repo" problem.**
251
184
 
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.
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
- ## Architecture
189
+ ## Roadmap (Future Improvements)
267
190
 
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 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 and appreciated.
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
- For bugs or feature requests, please [open an issue](https://github.com/lakindudev/toren/issues). Try to include a clear description and, for bugs, the output of `toren --doctor`.
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
- **Code style notes:**
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
- [MIT](LICENSE) free to use, modify, and distribute.
214
+ Distributed under the MIT License. See `LICENSE` for more information.
351
215
 
352
216
  ---
353
217
 
354
- *If Toren saves you time, consider starring the repository — it helps others discover the project.* ⭐
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] Scan a directory (defaults to current directory)
46
- toren --format <type> Select output format (default: console)
47
- toren --help Show this help message
48
- toren --version Show version number
49
- toren --doctor Check global installation health
50
- toren --uninstall Safely remove global installation
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 . Scan the current directory
59
- toren ./my-project Scan a specific project folder
60
- toren --format json Output results as JSON
61
- toren --format json . Scan a path and output as JSON
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] - Resolved path to scan
73
- * @property {string} [format] - Renderer format name
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('-v') || args.includes('--v')) {
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
- // Accept the token immediately following --format.
118
- // If the user omits the value (e.g. toren --format) default is used.
119
- format = args[formatIdx + 1] ?? DEFAULT_FORMAT;
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(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json']);
138
- const unknownFlag = args.find(a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a));
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
- if (parsed.action === 'exit') process.exit(0);
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.1",
4
- "description": "A CLI that analyzes software codebases and generates onboarding insights.",
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": "./bin/toren.js"
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
- "onboarding",
23
- "developer-tool",
24
- "scanner"
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.count >= PREVIEW_LIMIT) return;
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) break;
121
- renderTree(children[i], prefix + extension, i === children.length - 1, counter, depth + 1, maxDepth);
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) break;
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) {
@@ -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('.') && dirent.isDirectory()) return true;
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` and `entryPoints` arrays by reference.
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 - Accumulator for all relative file paths
141
- * @param {Array<string>} entryPoints - Accumulator for entry-point relative paths
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, entryPoints) {
140
+ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
145
141
  const name = path.basename(dirPath);
146
- const relPath = path.relative(rootPath, dirPath) || '.';
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, entryPoints);
181
+ const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden, maxFiles);
180
182
  node.children.push(childNode);
181
183
  } else if (dirent.isFile()) {
182
- const relFilePath = path.relative(rootPath, childPath);
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']) return 'Next.js';
244
- if (deps['react']) return 'React';
245
- if (deps['vue']) return 'Vue.js';
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']) return 'Svelte';
248
- if (deps['express']) return 'Node.js / Express';
249
- if (deps['fastify']) return 'Node.js / Fastify';
250
- if (deps['koa']) return 'Node.js / Koa';
251
- if (deps['typescript']) return 'Node.js / 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
- const entryPoints = [];
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, entryPoints);
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
- if (isEntryPoint(relFilePath)) {
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
- }