@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 CHANGED
@@ -1,164 +1,220 @@
1
- # Toren — Codebase Intelligence CLI
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 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.
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 structureall in milliseconds.
15
15
 
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.
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
- 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.
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 the **Toren CLI tool** globally via npm to use it across all your local projects:
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 Examples
53
-
54
- Navigate to your target project folder or point Toren directly to a repository.
44
+ ## CLI Usage
55
45
 
56
- Scan the current directory for an instant summary:
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
- toren .
60
- ```
49
+ # Scan a specific path
50
+ toren /path/to/project
61
51
 
62
- Scan and output the analysis in JSON format (ideal for toolchain integrations):
52
+ # Scan the current directory
53
+ toren .
63
54
 
64
- ```bash
65
- toren . --format json
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
- Run a deep analysis on the codebase:
72
+ ### All Flags
69
73
 
70
- ```bash
71
- toren . --analyze
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
- *Note: You can also point Toren to any absolute or relative path, e.g., `toren ../my-project`.*
85
+ > **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
75
86
 
76
87
  ---
77
88
 
78
- ## Supported Frameworks
79
-
80
- Toren is equipped with highly accurate **framework detection** for modern development stacks. It automatically detects:
89
+ ## Output Examples
81
90
 
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**
91
+ ### Console Output Example
92
+ The default `console` format renders a beautiful summary directly in your terminal:
95
93
 
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
-
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: Next.js
109
- Total files: 48
110
- Total folders: 11
111
- 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
112
104
 
113
105
  🚪 Entry Points
114
106
  ────────────────────────────────────────────────────────────────────────────────
115
- → src/app/page.tsx
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
- │ └── favicon.ico
113
+ │ └── vite.svg
123
114
  ├── src/
124
- │ ├── app/
125
- │ │ ├── layout.tsx
126
- │ └── page.tsx
127
- │ ├── components/
128
- ├── Header.tsx
129
- └── Footer.tsx
130
- │ └── lib/
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
- └── tsconfig.json
136
- … and 28 more file(s) not shown
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 **repository analyzer** and **developer tool** are welcome!
200
+ Contributions to improve this codebase analyzer CLI are always welcome!
147
201
 
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.
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 any additions maintain the zero-dependency architecture.
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
- [MIT](LICENSE) free to use, modify, and distribute.
214
+ Distributed under the MIT License. See `LICENSE` for more information.
161
215
 
162
216
  ---
163
217
 
164
- *If Toren saves you time during your codebase analysis, please consider starring the repository — it helps other developers discover this 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,9 +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
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('-v') || args.includes('--v')) {
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
- // Accept the token immediately following --format.
119
- // If the user omits the value (e.g. toren --format) default is used.
120
- 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;
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(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json', '--include-hidden']);
143
- const unknownFlag = args.find(a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a));
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
- if (parsed.action === 'exit') process.exit(0);
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, { includeHidden: parsed.includeHidden });
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.2",
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",
@@ -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 (err) {
153
- // Permission-denied or unreadable directory — skip and log warnings instead of failing.
154
- console.warn(`[warn] Skipping ${dirPath} (permission denied)`);
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']) return 'Next.js';
233
- if (deps['react']) return 'React';
234
- 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';
235
248
  if (deps['@angular/core']) return 'Angular';
236
- if (deps['svelte']) return 'Svelte';
237
- if (deps['express']) return 'Node.js / Express';
238
- if (deps['fastify']) return 'Node.js / Fastify';
239
- if (deps['koa']) return 'Node.js / Koa';
240
- 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';
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 (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'];
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 missed projects
335
- let best = null;
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
- }