@lakindu_perera/toren 1.0.2 → 1.0.4

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,247 @@
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.
55
-
56
- Scan the current directory for an instant summary:
57
-
58
- ```bash
59
- toren .
60
- ```
44
+ ## CLI Usage
61
45
 
62
- Scan and output the analysis in JSON format (ideal for toolchain integrations):
46
+ Toren is designed to be simple and intuitive. Point it at any directory to generate an immediate intelligence report.
63
47
 
64
48
  ```bash
65
- toren . --format json
66
- ```
49
+ # Scan a specific path
50
+ toren /path/to/project
67
51
 
68
- Run a deep analysis on the codebase:
52
+ # Scan the current directory
53
+ toren .
69
54
 
70
- ```bash
71
- toren . --analyze
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
+ # Focused Output Modes (Mutually Exclusive)
66
+ toren --project-type
67
+ toren --frameworks
68
+ toren --entry-points
69
+ toren --structure
70
+
71
+ # Lifecycle & Help Commands
72
+ toren --help
73
+ toren --version
74
+ toren --doctor
75
+ toren --uninstall
72
76
  ```
73
77
 
74
- *Note: You can also point Toren to any absolute or relative path, e.g., `toren ../my-project`.*
78
+ ### All Flags
79
+
80
+ | Flag | Description |
81
+ |------|-------------|
82
+ | `[path]` | Directory to scan. Defaults to the current directory (`.`). |
83
+ | `--project-type` | Show detected project type only. |
84
+ | `--frameworks` | Show detected frameworks only. |
85
+ | `--entry-points` | Show detected entry points only. |
86
+ | `--structure` | Show repository structure only. |
87
+ | `--format <type>` | Output format: `console` (default), `json`, `markdown`, `html`. |
88
+ | `--include-hidden` | Include hidden files and dot-directories in the scan. |
89
+ | `--max-files <N>` | Override the default 50,000-file scan limit. |
90
+ | `--help` / `-h` | Show usage information. |
91
+ | `--version` / `-V` | Print the installed version number. |
92
+ | `--doctor` | Diagnose the global installation health. |
93
+ | `--uninstall` | Safely remove Toren from the global npm environment. |
94
+
95
+ > **Note:** `--format md` is not a valid alias. Use `--format markdown` in full.
96
+ > **Note:** Focused output flags (`--project-type`, `--frameworks`, `--entry-points`, `--structure`) are mutually exclusive.
75
97
 
76
98
  ---
77
99
 
78
- ## Supported Frameworks
100
+ ## Output Examples
79
101
 
80
- Toren is equipped with highly accurate **framework detection** for modern development stacks. It automatically detects:
102
+ ### Focused Output Example
103
+ Sometimes you only need a specific piece of intelligence for use in a script or a quick lookup. Use the focused output flags to bypass the full report:
81
104
 
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**
105
+ ```bash
106
+ $ toren --project-type
107
+ Project Type: Node.js / JavaScript
95
108
 
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.
109
+ $ toren --frameworks
110
+ Frameworks:
111
+ - React
97
112
 
98
- ---
113
+ $ toren --entry-points
114
+ Entry Points:
115
+ - src/main.tsx
116
+ ```
99
117
 
100
- ## Example Output
118
+ ### Console Output Example
119
+ The default `console` format renders a beautiful summary directly in your terminal:
101
120
 
102
- ```
103
- Toren v1.0.1 — Codebase Onboarding Intelligence
121
+ ```text
122
+ Toren v1.0.4 — Codebase Onboarding Intelligence
104
123
 
105
124
  🔍 Project Summary
106
125
  ────────────────────────────────────────────────────────────────────────────────
107
- Path: ./my-app
108
- Project type: Next.js
109
- Total files: 48
110
- Total folders: 11
111
- Scan duration: 3 ms
126
+ Path: ./my-react-app
127
+ Project type: React
128
+ Total files: 32
129
+ Total folders: 6
130
+ Scan duration: 4 ms
112
131
 
113
132
  🚪 Entry Points
114
133
  ────────────────────────────────────────────────────────────────────────────────
115
- → src/app/page.tsx
116
- → src/app/layout.tsx
134
+ → src/main.tsx
117
135
 
118
136
  📁 Folder Structure (first 20 files)
119
137
  ────────────────────────────────────────────────────────────────────────────────
120
- my-app/
138
+ my-react-app/
121
139
  ├── public/
122
- │ └── favicon.ico
140
+ │ └── vite.svg
123
141
  ├── 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
142
+ │ ├── assets/
143
+ │ │ └── react.svg
144
+ ├── App.css
145
+ │ ├── App.tsx
146
+ │ ├── index.css
147
+ │ └── main.tsx
148
+ ├── index.html
134
149
  ├── package.json
135
- └── tsconfig.json
136
- … and 28 more file(s) not shown
150
+ └── vite.config.ts
151
+ … and 21 more file(s) not shown
137
152
 
138
153
  ────────────────────────────────────────────────────────────────────────────────
139
154
  ✅ Scan complete.
140
155
  ```
141
156
 
157
+ ### JSON Output Example
158
+ Generate machine-readable output for scripts, toolchains, or AI context windows by running `toren --format json`:
159
+
160
+ ```json
161
+ {
162
+ "project": {
163
+ "path": "./my-react-app",
164
+ "type": "React",
165
+ "framework": "React"
166
+ },
167
+ "summary": {
168
+ "totalFiles": 32,
169
+ "totalFolders": 6,
170
+ "scanDurationMs": 4
171
+ },
172
+ "entryPoints": [
173
+ "src/main.tsx"
174
+ ],
175
+ "structure": [
176
+ {
177
+ "type": "folder",
178
+ "name": "src",
179
+ "children": [
180
+ { "type": "file", "name": "App.tsx" },
181
+ { "type": "file", "name": "main.tsx" }
182
+ ]
183
+ },
184
+ { "type": "file", "name": "package.json" }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ *Note: You can also generate rich, GitHub-flavored Markdown (`--format markdown`) or self-contained HTML reports (`--format html`) for documentation purposes!*
190
+
191
+ ---
192
+
193
+ ## Architecture
194
+
195
+ Toren's internal architecture emphasizes modular design, separation of concerns, and a strict **zero dependency** philosophy.
196
+
197
+ - **`bin/toren.js`** — The CLI entry point. Handles argument parsing, option validation, and orchestrates the scanning and rendering phases.
198
+ - **`src/scanner/scan.js`** — The core scanning engine. Safely traverses the file system, executes framework detection heuristics, and extracts entry points.
199
+ - **`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.
200
+ - **`src/lifecycle.js`** — Dedicated install/uninstall diagnostic tools (`--doctor`, `--uninstall`) to ensure the global CLI binary remains healthy.
201
+
202
+ ---
203
+
204
+ ## Why Toren Exists
205
+
206
+ Modern software development moves fast, but **onboarding into large codebases is slow**.
207
+
208
+ 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.
209
+
210
+ **Toren solves the "first 5 minutes of any repo" problem.**
211
+
212
+ 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.
213
+
214
+ ---
215
+
216
+ ## Roadmap (Future Improvements)
217
+
218
+ - `.torenignore` configuration file support
219
+ - Dependency graph analysis and visualization
220
+ - Plugin system for custom renderer injection
221
+ - AI-generated natural language project summaries
222
+
142
223
  ---
143
224
 
144
225
  ## Contributing
145
226
 
146
- Contributions to improve this **repository analyzer** and **developer tool** are welcome!
227
+ Contributions to improve this codebase analyzer CLI are always welcome!
147
228
 
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.
229
+ 1. Fork the repository
230
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
231
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
232
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
233
+ 5. Open a Pull Request
153
234
 
154
- Please ensure any additions maintain the zero-dependency architecture.
235
+ *Please ensure all new features maintain the strict zero-dependency architecture rule.*
155
236
 
156
237
  ---
157
238
 
158
- ## License
239
+ ## 📄 License
159
240
 
160
- [MIT](LICENSE) free to use, modify, and distribute.
241
+ Distributed under the MIT License. See `LICENSE` for more information.
161
242
 
162
243
  ---
163
244
 
164
- *If Toren saves you time during your codebase analysis, please consider starring the repository — it helps other developers discover this project.* ⭐
245
+ ## 👨‍💻 Author
246
+
247
+ 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
  * ──────────────────────────
@@ -23,6 +25,7 @@
23
25
  import { createRequire } from 'node:module';
24
26
  import { scan } from '../src/scanner/scan.js';
25
27
  import renderers from '../src/renderers/index.js';
28
+ import { getFocusedModeInfo, renderFocusedMode, FOCUSED_FLAGS } from '../src/focused-output.js';
26
29
  import { runDoctor, runUninstall } from '../src/lifecycle.js';
27
30
 
28
31
  const require = createRequire(import.meta.url);
@@ -42,12 +45,20 @@ function printHelp() {
42
45
  const formatList = SUPPORTED_FORMATS.map(f => ` ${f}`).join('\n');
43
46
  console.log(`
44
47
  \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
48
+ toren [path] [options]
49
+
50
+ \x1b[1mOptions:\x1b[0m
51
+ --project-type Show detected project type only
52
+ --frameworks Show detected frameworks only
53
+ --entry-points Show detected entry points only
54
+ --structure Show repository structure only
55
+ --format <type> Output as console, json, markdown, or html
56
+ --include-hidden Include hidden files and folders
57
+ --max-files <n> Set scan file limit
58
+ --help Show this help message
59
+ --version Show version number
60
+ --doctor Run CLI diagnostics
61
+ --uninstall Remove Toren global installation
51
62
 
52
63
  \x1b[1mOutput Formats:\x1b[0m
53
64
  ${formatList}
@@ -55,10 +66,13 @@ ${formatList}
55
66
  console is the default.
56
67
 
57
68
  \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
69
+ toren . Scan the current directory
70
+ toren ./my-project Scan a specific project folder
71
+ toren --format json . Output results as JSON
72
+ toren --format markdown . > out.md Save a Markdown report to a file
73
+ toren --format html . > out.html Save an HTML report to a file
74
+ toren --include-hidden . Include hidden dot-files in scan
75
+ toren --max-files 100000 . Override the 50k file scan limit
62
76
  `);
63
77
  }
64
78
 
@@ -69,9 +83,10 @@ ${formatList}
69
83
  /**
70
84
  * @typedef {Object} ParsedArgs
71
85
  * @property {'exit'|'wait'|'scan'} action
72
- * @property {string} [target] - Resolved path to scan
73
- * @property {string} [format] - Renderer format name
86
+ * @property {string} [target] - Resolved path to scan
87
+ * @property {string} [format] - Renderer format name
74
88
  * @property {boolean} [includeHidden] - Whether to include hidden files
89
+ * @property {number} [maxFiles] - Max files to scan
75
90
  */
76
91
 
77
92
  /**
@@ -89,18 +104,18 @@ function parseArgs() {
89
104
  // ── Informational flags ─────────────────────────────────────────────────
90
105
  if (args.includes('--help') || args.includes('-h')) {
91
106
  printHelp();
92
- return { action: 'exit' };
107
+ return { action: 'exit', code: 0 };
93
108
  }
94
109
 
95
- if (args.includes('--version') || args.includes('-v') || args.includes('--v')) {
110
+ if (args.includes('--version') || args.includes('-V') || args.includes('-v')) {
96
111
  console.log(pkg.version);
97
- return { action: 'exit' };
112
+ return { action: 'exit', code: 0 };
98
113
  }
99
114
 
100
115
  // ── Lifecycle commands ──────────────────────────────────────────────────
101
116
  if (args.includes('--doctor')) {
102
117
  runDoctor(pkg.version);
103
- return { action: 'exit' };
118
+ return { action: 'exit', code: 0 };
104
119
  }
105
120
 
106
121
  if (args.includes('--uninstall')) {
@@ -115,40 +130,93 @@ function parseArgs() {
115
130
 
116
131
  const formatIdx = args.indexOf('--format');
117
132
  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;
133
+ const nextToken = args[formatIdx + 1];
134
+ // If the next token is missing or starts with '-', the user omitted the value.
135
+ if (nextToken === undefined || nextToken.startsWith('-')) {
136
+ console.error('');
137
+ console.error('\x1b[31m --format requires a value.\x1b[0m');
138
+ console.error('');
139
+ console.error(` Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
140
+ console.error('');
141
+ console.error(' Run \x1b[36mtoren --help\x1b[0m for usage.');
142
+ console.error('');
143
+ return { action: 'exit', code: 1 };
144
+ }
145
+ format = nextToken;
121
146
  } else if (args.includes('--json')) {
122
147
  format = 'json';
123
148
  }
124
149
 
150
+ // ── Focused Output Flags ────────────────────────────────────────────────
151
+ const focusedInfo = getFocusedModeInfo(args);
152
+
153
+ if (focusedInfo.error) {
154
+ console.error(focusedInfo.message);
155
+ return { action: 'exit', code: 1 };
156
+ }
157
+
158
+ const focusedMode = focusedInfo.mode;
159
+
125
160
  // ── Hidden files ────────────────────────────────────────────────────────
126
161
  const includeHidden = args.includes('--include-hidden');
127
162
 
128
-
129
163
  // ── Target path ─────────────────────────────────────────────────────────
130
164
  // Build the set of tokens that are consumed as values by named flags so
131
165
  // we don't accidentally treat them as the positional path argument.
132
- // Currently only --format consumes a value token.
133
166
  const consumedValues = new Set();
134
- if (formatIdx !== -1 && args[formatIdx + 1] !== undefined) {
167
+ if (formatIdx !== -1 && args[formatIdx + 1] !== undefined && !args[formatIdx + 1].startsWith('-')) {
135
168
  consumedValues.add(args[formatIdx + 1]);
136
169
  }
137
170
 
171
+ // ── Max files ───────────────────────────────────────────────────────────
172
+ let maxFiles = undefined;
173
+ const maxFilesIdx = args.indexOf('--max-files');
174
+ if (maxFilesIdx !== -1) {
175
+ const rawVal = args[maxFilesIdx + 1];
176
+ if (rawVal === undefined || rawVal.startsWith('-')) {
177
+ console.error('');
178
+ console.error('\x1b[31m --max-files requires a numeric value.\x1b[0m');
179
+ console.error(' Example: toren --max-files 100000');
180
+ console.error('');
181
+ return { action: 'exit', code: 1 };
182
+ }
183
+ maxFiles = parseInt(rawVal, 10);
184
+ if (isNaN(maxFiles) || maxFiles < 1) {
185
+ console.error('');
186
+ console.error(`\x1b[31m --max-files must be a positive integer, got: ${rawVal}\x1b[0m`);
187
+ console.error('');
188
+ return { action: 'exit', code: 1 };
189
+ }
190
+ consumedValues.add(rawVal);
191
+ }
192
+
138
193
  // First non-flag, non-consumed token is the target path; default to cwd.
139
194
  const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
140
195
 
141
196
  // ── 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
-
197
+ const knownFlags = new Set([
198
+ '--help', '-h',
199
+ '--version', '-V', '-v',
200
+ '--doctor',
201
+ '--uninstall',
202
+ '--format',
203
+ '--json',
204
+ ...FOCUSED_FLAGS,
205
+ '--include-hidden',
206
+ '--max-files',
207
+ ]);
208
+
209
+ const unknownFlag = args.find(
210
+ a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a),
211
+ );
212
+
145
213
  if (unknownFlag) {
146
214
  console.error(`\x1b[31m Unknown flag: ${unknownFlag}\x1b[0m`);
147
215
  console.error(` Run \x1b[36mtoren --help\x1b[0m for usage.\n`);
148
- return { action: 'exit' };
216
+ return { action: 'exit', code: 1 };
149
217
  }
150
218
 
151
- return { action: 'scan', target, format, includeHidden };
219
+ return { action: 'scan', target, format, includeHidden, maxFiles, focusedMode };
152
220
  }
153
221
 
154
222
  // ---------------------------------------------------------------------------
@@ -183,7 +251,10 @@ function assertValidFormat(format) {
183
251
 
184
252
  (function main() {
185
253
  const parsed = parseArgs();
186
- if (parsed.action === 'exit') process.exit(0);
254
+
255
+ if (parsed.action === 'exit') {
256
+ process.exit(parsed.code ?? 0);
257
+ }
187
258
  if (parsed.action === 'wait') return;
188
259
 
189
260
  // Validate before scanning — fail fast on bad format names.
@@ -192,8 +263,16 @@ function assertValidFormat(format) {
192
263
  const render = renderers[parsed.format];
193
264
 
194
265
  try {
195
- const result = scan(parsed.target, { includeHidden: parsed.includeHidden });
196
- render(result, { cwd: process.cwd() });
266
+ const result = scan(parsed.target, {
267
+ includeHidden: parsed.includeHidden,
268
+ maxFiles: parsed.maxFiles,
269
+ });
270
+
271
+ if (parsed.focusedMode) {
272
+ renderFocusedMode(parsed.focusedMode, result);
273
+ } else {
274
+ render(result, { cwd: process.cwd() });
275
+ }
197
276
  } catch (err) {
198
277
  // Render errors in the requested format where possible.
199
278
  if (parsed.format === 'json') {
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.4",
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",
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Focused output modes handling for Toren CLI.
3
+ */
4
+
5
+ import { renderStructure } from './renderers/console-renderer.js';
6
+
7
+ export const FOCUSED_FLAGS = [
8
+ '--project-type',
9
+ '--frameworks',
10
+ '--entry-points',
11
+ '--structure'
12
+ ];
13
+
14
+ /**
15
+ * Validates focused flags in the provided arguments.
16
+ *
17
+ * @param {string[]} args Process arguments
18
+ * @returns {{ error: boolean, message?: string, mode?: string|null }}
19
+ */
20
+ export function getFocusedModeInfo(args) {
21
+ const activeFlags = FOCUSED_FLAGS.filter(flag => args.includes(flag));
22
+
23
+ if (activeFlags.length > 1) {
24
+ return {
25
+ error: true,
26
+ message: '\x1b[31mError: focused output flags are mutually exclusive. Please use only one of:\x1b[0m\n' +
27
+ FOCUSED_FLAGS.map(f => ` ${f}`).join('\n') + '\n'
28
+ };
29
+ }
30
+
31
+ return {
32
+ error: false,
33
+ mode: activeFlags[0] || null
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Renders the scan result based on the active focused mode.
39
+ *
40
+ * @param {string} mode Active focused mode flag
41
+ * @param {import('./scanner/scan.js').ScanResult} result
42
+ */
43
+ export function renderFocusedMode(mode, result) {
44
+ switch (mode) {
45
+ case '--project-type':
46
+ console.log(`Project Type: ${result.projectType}`);
47
+ break;
48
+
49
+ case '--frameworks':
50
+ if (!result.projectType || result.projectType === 'Unknown') {
51
+ console.log('Frameworks: None detected');
52
+ } else {
53
+ console.log('Frameworks:');
54
+ console.log(`- ${result.projectType}`);
55
+ }
56
+ break;
57
+
58
+ case '--entry-points':
59
+ if (!result.entryPoints || result.entryPoints.length === 0) {
60
+ console.log('Entry Points: None detected');
61
+ } else {
62
+ console.log('Entry Points:');
63
+ result.entryPoints.forEach(ep => console.log(`- ${ep}`));
64
+ }
65
+ break;
66
+
67
+ case '--structure':
68
+ renderStructure(result);
69
+ break;
70
+ }
71
+ }
@@ -99,8 +99,11 @@ function row(label, value, ...valueCodes) {
99
99
  * @param {string} prefix - Accumulated indentation
100
100
  * @param {boolean} isLast - Whether this is the last sibling
101
101
  * @param {{ count: number, maxReached: boolean }} counter - Shared mutable file counter
102
+ * @param {number} limit - Max files to render
103
+ * @param {number} depth - Current depth
104
+ * @param {number} maxDepth - Max recursion depth
102
105
  */
103
- function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
106
+ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth = 0, maxDepth = 4) {
104
107
  if (counter.maxReached) return;
105
108
  if (depth >= maxDepth) return;
106
109
 
@@ -117,7 +120,7 @@ function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
117
120
  }
118
121
 
119
122
  for (let i = 0; i < children.length; i++) {
120
- if (counter.count >= PREVIEW_LIMIT) {
123
+ if (counter.count >= limit) {
121
124
  console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
122
125
  counter.maxReached = true;
123
126
  break;
@@ -125,7 +128,7 @@ function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
125
128
  // Check if this child will be the last one we render due to limits
126
129
  let willBeLast = i === children.length - 1;
127
130
 
128
- renderTree(children[i], prefix + extension, willBeLast, counter, depth + 1, maxDepth);
131
+ renderTree(children[i], prefix + extension, willBeLast, counter, limit, depth + 1, maxDepth);
129
132
  if (counter.maxReached) break;
130
133
  }
131
134
  } else {
@@ -228,3 +231,28 @@ export function render(result, options = {}) {
228
231
  console.log(paint(' ✅ Scan complete.', C.green));
229
232
  console.log('');
230
233
  }
234
+
235
+ /**
236
+ * Render only the project structure for the --structure flag.
237
+ *
238
+ * @param {import('../scanner/scan.js').ScanResult} result
239
+ */
240
+ export function renderStructure(result) {
241
+ const { tree, flatFiles } = result;
242
+
243
+ console.log(paint('Project Structure:', C.bold, C.white));
244
+ console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
245
+
246
+ const counter = { count: 0, maxReached: false };
247
+ const children = tree.children ?? [];
248
+ const limit = flatFiles.length; // No preview limit for focused --structure
249
+
250
+ for (let i = 0; i < children.length; i++) {
251
+ if (counter.count >= limit) {
252
+ console.log(`└── ${paint('...', C.dim)}`);
253
+ break;
254
+ }
255
+ renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
256
+ if (counter.maxReached) break;
257
+ }
258
+ }
@@ -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
- }