@lakindu_perera/toren 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Toren
1
+ # Toren — Codebase Intelligence CLI
2
2
 
3
3
  > Understand any codebase in seconds.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/toren.svg)](https://www.npmjs.com/package/toren)
5
+ [![npm version](https://img.shields.io/npm/v/@lakindu_perera/toren.svg)](https://www.npmjs.com/package/@lakindu_perera/toren)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
8
8
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-success.svg)](package.json)
@@ -11,85 +11,96 @@
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.
14
+ **Toren** is a powerful **codebase intelligence CLI** and **project analysis** tool designed to help developers understand unfamiliar repositories instantly. By running this lightweight **scanner**, you gain immediate insight into the architecture of any software project. It performs robust **framework detection**, identifies critical application start files through **entry point analysis**, and generates a clear **project structure visualization**—all without requiring you to read a single line of code.
15
15
 
16
- 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
22
-
23
- Whether you've just cloned an open-source project, joined a new team, or are reviewing a client's codebase, Toren cuts through the noise and gets you oriented fast.
16
+ Whether you are onboarding to a new team, auditing a complex repository, or looking for a fast **monorepo analyzer**, Toren cuts through the noise and gets you oriented fast.
24
17
 
25
18
  ---
26
19
 
27
20
  ## Features
28
21
 
29
- - **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
+ Toren is built to deliver comprehensive repository insights efficiently:
23
+
24
+ - **Codebase Scanning**: Recursively walks any project in milliseconds to gather deep architectural insights.
25
+ - **Framework Detection**: Instantly identifies the underlying technologies powering the application.
26
+ - **Entry Point Analysis**: Automatically pinpoints where execution begins (e.g., `index.js`, `main.ts`, `App.tsx`, `Application.java`).
27
+ - **Project Structure Visualization**: Generates a beautiful, hierarchical directory tree right in your terminal.
28
+ - **Monorepo Support**: Gracefully handles complex, multi-package repositories without failing.
29
+ - **Zero Dependencies**: A pure Node.js CLI tool with zero external runtime packages.
30
+ - **Machine-Readable Output**: Full JSON support for seamless integration with other developer tools and AI agents.
38
31
 
39
32
  ---
40
33
 
41
34
  ## Installation
42
35
 
43
- Install globally with npm:
36
+ Install the **Toren CLI tool** globally via npm to use it across all your local projects:
44
37
 
45
38
  ```bash
46
- npm install -g toren
39
+ npm install -g @lakindu_perera/toren
47
40
  ```
48
41
 
49
- Or run without installing:
42
+ Alternatively, you can run it instantly without global installation:
50
43
 
51
44
  ```bash
52
- npx toren
45
+ npx @lakindu_perera/toren
53
46
  ```
54
47
 
55
48
  **Requirements:** Node.js 18.0.0 or higher.
56
49
 
57
50
  ---
58
51
 
59
- ## Quick Start
60
-
61
- Scan the current directory:
52
+ ## Usage Examples
62
53
 
63
- ```bash
64
- toren
65
- ```
54
+ Navigate to your target project folder or point Toren directly to a repository.
66
55
 
67
- Scan a specific path:
56
+ Scan the current directory for an instant summary:
68
57
 
69
58
  ```bash
70
59
  toren .
71
- toren ../my-project
72
- toren /path/to/any/repo
73
60
  ```
74
61
 
75
- Output results as JSON:
62
+ Scan and output the analysis in JSON format (ideal for toolchain integrations):
76
63
 
77
64
  ```bash
78
- toren --json
65
+ toren . --format json
79
66
  ```
80
67
 
81
- Check your global installation:
68
+ Run a deep analysis on the codebase:
82
69
 
83
70
  ```bash
84
- toren --doctor
71
+ toren . --analyze
85
72
  ```
86
73
 
74
+ *Note: You can also point Toren to any absolute or relative path, e.g., `toren ../my-project`.*
75
+
76
+ ---
77
+
78
+ ## Supported Frameworks
79
+
80
+ Toren is equipped with highly accurate **framework detection** for modern development stacks. It automatically detects:
81
+
82
+ - **Next.js**
83
+ - **React**
84
+ - **Node.js** (including Express, Fastify, Koa, and TypeScript variants)
85
+ - **Spring Boot** (Java)
86
+ - **Python** (including Pipenv and pyproject.toml setups)
87
+ - **Go**
88
+ - **Rust**
89
+ - **Vue.js**
90
+ - **Angular**
91
+ - **Svelte**
92
+ - **PHP**
93
+ - **Ruby**
94
+ - **Elixir**
95
+
96
+ If a specific marker is not found, Toren intelligently falls back to structural heuristics to identify generic entry points, ensuring you always get meaningful project analysis.
97
+
87
98
  ---
88
99
 
89
100
  ## Example Output
90
101
 
91
102
  ```
92
- Toren v1.0.0 — Codebase Onboarding Intelligence
103
+ Toren v1.0.1 — Codebase Onboarding Intelligence
93
104
 
94
105
  🔍 Project Summary
95
106
  ────────────────────────────────────────────────────────────────────────────────
@@ -130,218 +141,17 @@ my-app/
130
141
 
131
142
  ---
132
143
 
133
- ## JSON Output
134
-
135
- Use `toren --json` to get a machine-readable result suitable for piping into other tools:
136
-
137
- ```json
138
- {
139
- "project": {
140
- "path": "./my-app",
141
- "type": "Next.js",
142
- "framework": "Next.js"
143
- },
144
- "summary": {
145
- "totalFiles": 48,
146
- "totalFolders": 11,
147
- "scanDurationMs": 3
148
- },
149
- "entryPoints": [
150
- "src/app/page.tsx",
151
- "src/app/layout.tsx"
152
- ],
153
- "structure": [
154
- {
155
- "type": "folder",
156
- "name": "src",
157
- "children": [
158
- {
159
- "type": "folder",
160
- "name": "app",
161
- "children": [
162
- { "type": "file", "name": "layout.tsx" },
163
- { "type": "file", "name": "page.tsx" }
164
- ]
165
- }
166
- ]
167
- },
168
- { "type": "file", "name": "package.json" },
169
- { "type": "file", "name": "next.config.js" }
170
- ]
171
- }
172
- ```
173
-
174
- ---
175
-
176
- ## Supported Project Types
177
-
178
- Toren detects the following project types automatically:
179
-
180
- | Marker File | Detected Type |
181
- |--------------------------------------|----------------------------|
182
- | `package.json` | Node.js / JavaScript |
183
- | `package.json` + `next` dep | Next.js |
184
- | `package.json` + `react` dep | React |
185
- | `package.json` + `vue` dep | Vue.js |
186
- | `package.json` + `@angular/core` dep | Angular |
187
- | `package.json` + `svelte` dep | Svelte |
188
- | `package.json` + `express` dep | Node.js / Express |
189
- | `package.json` + `fastify` dep | Node.js / Fastify |
190
- | `package.json` + `koa` dep | Node.js / Koa |
191
- | `package.json` + `typescript` dep | Node.js / TypeScript |
192
- | `requirements.txt` | Python |
193
- | `Pipfile` | Python (Pipenv) |
194
- | `pyproject.toml` | Python (pyproject) |
195
- | `go.mod` | Go |
196
- | `Cargo.toml` | Rust |
197
- | `pom.xml` | Java / Spring Boot |
198
- | `build.gradle` | Java / Gradle |
199
- | `composer.json` | PHP / Composer |
200
- | `Gemfile` | Ruby |
201
- | `mix.exs` | Elixir |
202
-
203
- If no marker is found, Toren reports `Unknown` without failing.
204
-
205
- ---
206
-
207
- ## CLI Reference
208
-
209
- | Command | Description |
210
- |----------------------|--------------------------------------------------|
211
- | `toren` | Scan the current directory |
212
- | `toren [path]` | Scan a specific directory or file path |
213
- | `toren --json` | Output scan results as formatted JSON |
214
- | `toren --version` | Print the installed version number |
215
- | `toren --help` | Show usage information |
216
- | `toren --doctor` | Diagnose the global installation health |
217
- | `toren --uninstall` | Guided removal of the global installation |
218
-
219
- ---
220
-
221
- ## Project Structure
222
-
223
- ```
224
- toren/
225
- ├── bin/
226
- │ └── toren.js # CLI entry point — argument parsing and orchestration
227
- ├── src/
228
- │ ├── lifecycle.js # --doctor and --uninstall command implementations
229
- │ ├── scanner/
230
- │ │ └── scan.js # Core scanning engine — file walker, type detection, entry point detection
231
- │ └── renderers/
232
- │ ├── console-renderer.js # ANSI-styled terminal output
233
- │ ├── json-renderer.js # Machine-readable JSON output
234
- │ └── tree-renderer.js # Standalone flat-file tree formatter (utility)
235
- └── package.json
236
- ```
237
-
238
- ---
239
-
240
- ## How It Works
241
-
242
- ### 1. Scanner
243
-
244
- `src/scanner/scan.js` is the core engine. It takes a target path and recursively walks the file system using Node's `fs.readdirSync`, collecting every file and directory while skipping entries in the ignore list (`node_modules`, `.git`, `dist`, `build`, etc.).
245
-
246
- The result is an in-memory tree of `DirNode` and `FileNode` objects, along with a flat list of all relative file paths.
247
-
248
- ### 2. Framework Detection
249
-
250
- After walking the directory, the scanner checks for known marker files at the project root (e.g. `package.json`, `go.mod`, `Cargo.toml`). For `package.json`, it reads the file and inspects `dependencies`, `devDependencies`, and `peerDependencies` to determine the specific framework (React, Next.js, Vue, Angular, etc.).
251
-
252
- ### 3. Entry Point Detection
253
-
254
- During the walk, each filename is checked against a known set of entry points: `index.js`, `index.ts`, `main.py`, `App.tsx`, `Application.java`, `page.tsx`, `layout.tsx`, etc. All matches are collected and surfaced in the output.
255
-
256
- ### 4. Renderer
257
-
258
- The scan result — a plain JavaScript object — is passed to a renderer. Renderers are completely decoupled from the scanner; they only read data and produce output.
259
-
260
- ### 5. Output
261
-
262
- The CLI selects the appropriate renderer based on flags (`--json` → JSON renderer; default → console renderer). Errors are caught and formatted consistently in both modes.
263
-
264
- ---
265
-
266
- ## Architecture
267
-
268
- ```
269
- CLI (bin/toren.js)
270
-
271
-
272
- Argument Parser
273
-
274
-
275
- Scanner (scan.js)
276
-
277
- ├── Directory Walker
278
- ├── Ignore Filter
279
- ├── Framework Detector
280
- └── Entry Point Detector
281
-
282
-
283
- ScanResult (plain object)
284
-
285
- ├──────────────────┐
286
- ▼ ▼
287
- Console Renderer JSON Renderer
288
- (ANSI terminal) (stdout / pipe)
289
- ```
290
-
291
- Adding a new output format is as simple as creating a new file in `src/renderers/` and importing it in `bin/toren.js`.
292
-
293
- ---
294
-
295
- ## Why Toren?
296
-
297
- When you encounter a new codebase, the usual approach is to start opening files, guessing at folder names, and reading `package.json` manually. This works — but it's slow and inconsistent.
298
-
299
- Toren automates that first pass. In one command, you get a structured summary of what the project is, where it starts, and what's inside. This is especially useful when:
300
-
301
- - **Onboarding to a new job** — quickly orient yourself before your first meeting
302
- - **Reviewing a pull request or open-source repo** — understand the scope at a glance
303
- - **Auditing a legacy codebase** — know what you're dealing with before diving in
304
- - **Building tooling** — use `--json` to feed project metadata into scripts or AI tools
305
-
306
- ---
307
-
308
- ## Roadmap
309
-
310
- - [x] Console renderer
311
- - [x] JSON renderer
312
- - [x] Framework detection (11+ project types)
313
- - [x] Entry point detection
314
- - [x] `--doctor` install health check
315
- - [x] `--uninstall` guided removal
316
- - [x] Markdown renderer (`--format md`)
317
- - [x] HTML renderer (`--format html`)
318
- - [ ] YAML output
319
- - [ ] `.torenignore` configuration file
320
- - [ ] Plugin / custom renderer system
321
- - [ ] Dependency graph analysis
322
- - [ ] AI-generated project summary
323
- - [ ] Project health score
324
- - [ ] Architecture visualisation layer
325
-
326
- ---
327
-
328
144
  ## Contributing
329
145
 
330
- Contributions 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
146
+ Contributions to improve this **repository analyzer** and **developer tool** are welcome!
337
147
 
338
- For bugs or feature requests, please [open an issue](https://github.com/your-username/toren/issues). Try to include a clear description and, for bugs, the output of `toren --doctor`.
148
+ 1. **Fork** this repository.
149
+ 2. **Create** a feature branch: `git checkout -b feature/my-feature`.
150
+ 3. **Commit** your changes: `git commit -m "feat: add my feature"`.
151
+ 4. **Push** to your branch: `git push origin feature/my-feature`.
152
+ 5. **Open** a Pull Request.
339
153
 
340
- **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
154
+ Please ensure any additions maintain the zero-dependency architecture.
345
155
 
346
156
  ---
347
157
 
@@ -351,4 +161,4 @@ For bugs or feature requests, please [open an issue](https://github.com/your-use
351
161
 
352
162
  ---
353
163
 
354
- *If Toren saves you time, consider starring the repository — it helps others discover the project.* ⭐
164
+ *If Toren saves you time during your codebase analysis, please consider starring the repository — it helps other developers discover this project.* ⭐
package/bin/toren.js CHANGED
@@ -71,6 +71,7 @@ ${formatList}
71
71
  * @property {'exit'|'wait'|'scan'} action
72
72
  * @property {string} [target] - Resolved path to scan
73
73
  * @property {string} [format] - Renderer format name
74
+ * @property {boolean} [includeHidden] - Whether to include hidden files
74
75
  */
75
76
 
76
77
  /**
@@ -121,6 +122,10 @@ function parseArgs() {
121
122
  format = 'json';
122
123
  }
123
124
 
125
+ // ── Hidden files ────────────────────────────────────────────────────────
126
+ const includeHidden = args.includes('--include-hidden');
127
+
128
+
124
129
  // ── Target path ─────────────────────────────────────────────────────────
125
130
  // Build the set of tokens that are consumed as values by named flags so
126
131
  // we don't accidentally treat them as the positional path argument.
@@ -134,7 +139,7 @@ function parseArgs() {
134
139
  const target = args.find(a => !a.startsWith('-') && !consumedValues.has(a)) ?? '.';
135
140
 
136
141
  // ── Unknown flag check ──────────────────────────────────────────────────
137
- const knownFlags = new Set(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json']);
142
+ const knownFlags = new Set(['--help', '-h', '--version', '-v', '--v', '--doctor', '--uninstall', '--format', '--json', '--include-hidden']);
138
143
  const unknownFlag = args.find(a => a.startsWith('-') && !knownFlags.has(a) && !consumedValues.has(a));
139
144
 
140
145
  if (unknownFlag) {
@@ -143,7 +148,7 @@ function parseArgs() {
143
148
  return { action: 'exit' };
144
149
  }
145
150
 
146
- return { action: 'scan', target, format };
151
+ return { action: 'scan', target, format, includeHidden };
147
152
  }
148
153
 
149
154
  // ---------------------------------------------------------------------------
@@ -187,7 +192,7 @@ function assertValidFormat(format) {
187
192
  const render = renderers[parsed.format];
188
193
 
189
194
  try {
190
- const result = scan(parsed.target);
195
+ const result = scan(parsed.target, { includeHidden: parsed.includeHidden });
191
196
  render(result, { cwd: process.cwd() });
192
197
  } catch (err) {
193
198
  // Render errors in the requested format where possible.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@lakindu_perera/toren",
3
- "version": "1.0.0",
4
- "description": "A CLI that analyzes software codebases and generates onboarding insights.",
3
+ "version": "1.0.2",
4
+ "description": "A powerful codebase scanner CLI tool for project analysis, framework detection, and understanding repository structure instantly.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "toren": "./bin/toren.js"
7
+ "toren": "bin/toren.js"
8
8
  },
9
9
  "preferGlobal": true,
10
10
  "main": "bin/toren.js",
@@ -17,16 +17,21 @@
17
17
  "lint": "node --check bin/toren.js src/scanner/scan.js"
18
18
  },
19
19
  "keywords": [
20
- "cli",
21
- "codebase",
22
- "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
+ "typescript cli",
29
+ "onboarding"
25
30
  ],
26
31
  "author": "Lakindu Perera",
27
32
  "repository": {
28
33
  "type": "git",
29
- "url": "https://github.com/lakindudev/toren.git"
34
+ "url": "git+https://github.com/lakindudev/toren.git"
30
35
  },
31
36
  "bugs": {
32
37
  "url": "https://github.com/lakindudev/toren/issues"
@@ -98,10 +98,10 @@ function row(label, value, ...valueCodes) {
98
98
  * @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
99
99
  * @param {string} prefix - Accumulated indentation
100
100
  * @param {boolean} isLast - Whether this is the last sibling
101
- * @param {{ count: number }} counter - Shared mutable file counter
101
+ * @param {{ count: number, maxReached: boolean }} counter - Shared mutable file counter
102
102
  */
103
103
  function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
104
- if (counter.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:
@@ -65,24 +66,6 @@ const PROJECT_TYPE_MARKERS = [
65
66
  { marker: 'mix.exs', label: 'Elixir' },
66
67
  ];
67
68
 
68
- const ENTRY_POINT_EXACT = new Set([
69
- 'index.js', 'index.ts', 'index.jsx', 'index.tsx',
70
- 'main.js', 'main.ts', 'main.jsx', 'main.tsx', 'main.py',
71
- 'app.js', 'app.ts', 'app.jsx', 'app.tsx', 'App.js', 'App.ts', 'App.jsx', 'App.tsx',
72
- 'server.js', 'server.ts', 'server.jsx', 'server.tsx',
73
- 'Application.java', 'Main.java',
74
- 'page.js', 'page.ts', 'page.jsx', 'page.tsx',
75
- 'layout.js', 'layout.ts', 'layout.jsx', 'layout.tsx',
76
- '_app.js', '_app.ts', '_app.jsx', '_app.tsx',
77
- '_document.js', '_document.ts', '_document.jsx', '_document.tsx'
78
- ]);
79
-
80
- function isEntryPoint(filename) {
81
- if (ENTRY_POINT_EXACT.has(filename)) return true;
82
- if (filename.endsWith('Application.java')) return true;
83
- return false;
84
- }
85
-
86
69
  // ---------------------------------------------------------------------------
87
70
  // Types (JSDoc — no TypeScript dependency required)
88
71
  // ---------------------------------------------------------------------------
@@ -92,7 +75,7 @@ function isEntryPoint(filename) {
92
75
  * @property {'file'} type
93
76
  * @property {string} name - Basename of the file
94
77
  * @property {string} fullPath - Absolute path
95
- * @property {string} relPath - Path relative to the scanned root
78
+ * @property {string} relPath - Path relative to the scanned root (POSIX style)
96
79
  */
97
80
 
98
81
  /**
@@ -100,7 +83,7 @@ function isEntryPoint(filename) {
100
83
  * @property {'directory'} type
101
84
  * @property {string} name - Basename of the directory
102
85
  * @property {string} fullPath - Absolute path
103
- * @property {string} relPath - Path relative to the scanned root
86
+ * @property {string} relPath - Path relative to the scanned root (POSIX style)
104
87
  * @property {Array<FileNode|DirNode>} children
105
88
  */
106
89
 
@@ -119,31 +102,40 @@ function isEntryPoint(filename) {
119
102
  // Internal helpers
120
103
  // ---------------------------------------------------------------------------
121
104
 
105
+ /**
106
+ * Normalize a path to use POSIX separators ('/').
107
+ */
108
+ function toPosix(p) {
109
+ return p.replace(/\\/g, '/');
110
+ }
111
+
122
112
  /**
123
113
  * Determine whether a directory entry should be skipped.
124
114
  *
125
115
  * @param {string} name - Basename of the entry
126
116
  * @param {fs.Dirent} dirent
117
+ * @param {boolean} includeHidden
127
118
  * @returns {boolean}
128
119
  */
129
- function shouldIgnore(name, dirent) {
130
- if (name.startsWith('.') && dirent.isDirectory()) return true;
120
+ function shouldIgnore(name, dirent, includeHidden) {
121
+ if (!includeHidden && name.startsWith('.')) return true;
131
122
  return IGNORED_DIRS.has(name);
132
123
  }
133
124
 
134
125
  /**
135
126
  * Recursively walk `dirPath`, building a DirNode tree.
136
- * Also populates `flatFiles` and `entryPoints` arrays by reference.
127
+ * Also populates `flatFiles` array by reference.
137
128
  *
138
129
  * @param {string} dirPath - Absolute path of the current directory
139
130
  * @param {string} rootPath - Absolute path of the scan root (for relative paths)
140
- * @param {Array<string>} flatFiles - Accumulator for all relative file paths
141
- * @param {Array<string>} entryPoints - Accumulator for entry-point relative paths
131
+ * @param {Array<string>} flatFiles - Accumulator for all relative file paths
132
+ * @param {boolean} includeHidden - Whether to include hidden files
142
133
  * @returns {DirNode}
143
134
  */
144
- function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
135
+ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden) {
145
136
  const name = path.basename(dirPath);
146
- const relPath = path.relative(rootPath, dirPath) || '.';
137
+ let rawRelPath = path.relative(rootPath, dirPath) || '.';
138
+ const relPath = toPosix(rawRelPath);
147
139
 
148
140
  /** @type {DirNode} */
149
141
  const node = {
@@ -157,8 +149,9 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
157
149
  let entries;
158
150
  try {
159
151
  entries = fs.readdirSync(dirPath, { withFileTypes: true });
160
- } catch {
161
- // Permission-denied or unreadable directory — skip silently.
152
+ } catch (err) {
153
+ // Permission-denied or unreadable directory — skip and log warnings instead of failing.
154
+ console.warn(`[warn] Skipping ${dirPath} (permission denied)`);
162
155
  return node;
163
156
  }
164
157
 
@@ -171,15 +164,15 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
171
164
  });
172
165
 
173
166
  for (const dirent of entries) {
174
- if (shouldIgnore(dirent.name, dirent)) continue;
167
+ if (shouldIgnore(dirent.name, dirent, includeHidden)) continue;
175
168
 
176
169
  const childPath = path.join(dirPath, dirent.name);
177
170
 
178
171
  if (dirent.isDirectory()) {
179
- const childNode = walkDirectory(childPath, rootPath, flatFiles, entryPoints);
172
+ const childNode = walkDirectory(childPath, rootPath, flatFiles, includeHidden);
180
173
  node.children.push(childNode);
181
174
  } else if (dirent.isFile()) {
182
- const relFilePath = path.relative(rootPath, childPath);
175
+ const relFilePath = toPosix(path.relative(rootPath, childPath));
183
176
 
184
177
  /** @type {FileNode} */
185
178
  const fileNode = {
@@ -191,10 +184,6 @@ function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
191
184
 
192
185
  node.children.push(fileNode);
193
186
  flatFiles.push(relFilePath);
194
-
195
- if (isEntryPoint(dirent.name)) {
196
- entryPoints.push(relFilePath);
197
- }
198
187
  }
199
188
  }
200
189
 
@@ -255,6 +244,124 @@ function refineNodeProjectType(pkgPath) {
255
244
  return 'Node.js / JavaScript';
256
245
  }
257
246
 
247
+ // ---------------------------------------------------------------------------
248
+ // Entry Point Heuristics
249
+ // ---------------------------------------------------------------------------
250
+
251
+ const FALSE_POSITIVES = [
252
+ '/internal/', '/renderer/', '/renderers/', '/dist/', '/build/', '/generated/', '/node_modules/'
253
+ ];
254
+
255
+ function isFalsePositive(relPath) {
256
+ const normalized = '/' + relPath + '/'; // relPath is already POSIX
257
+ if (FALSE_POSITIVES.some(fp => normalized.includes(fp))) return true;
258
+ if (relPath.includes('.test.') || relPath.includes('.spec.')) return true;
259
+ // Exclude config files usually not entry points
260
+ if (relPath.endsWith('.config.js') || relPath.endsWith('.config.ts')) return true;
261
+ return false;
262
+ }
263
+
264
+ function findEntryPoints(projectType, flatFiles, rootPath) {
265
+ let entries = [];
266
+ const validFiles = flatFiles.filter(f => !isFalsePositive(f));
267
+ const validSet = new Set(validFiles);
268
+
269
+ if (projectType === 'Node.js / JavaScript' || projectType.startsWith('Node.js')) {
270
+ try {
271
+ const pkgPath = path.join(rootPath, 'package.json');
272
+ if (fs.existsSync(pkgPath)) {
273
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
274
+ if (pkg.bin) {
275
+ if (typeof pkg.bin === 'string') entries.push(pkg.bin);
276
+ else Object.values(pkg.bin).forEach(b => entries.push(b));
277
+ }
278
+ if (pkg.main) entries.push(pkg.main);
279
+ }
280
+ } catch {}
281
+
282
+ // Normalize and filter package.json entries to ensure they exist
283
+ entries = entries.map(e => toPosix(e).replace(/^\.\//, '')).filter(e => validSet.has(e));
284
+
285
+ if (entries.length === 0) {
286
+ const fallbacks = ['src/index.ts', 'src/index.js', 'lib/index.js', 'index.js'];
287
+ for (const f of fallbacks) {
288
+ if (validSet.has(f)) { entries.push(f); break; }
289
+ }
290
+ }
291
+ } else if (projectType === 'React' || projectType === 'Next.js' || projectType === 'Vue.js' || projectType === 'Angular' || projectType === 'Svelte') {
292
+ const priorities = ['src/main.tsx', 'src/main.jsx', 'pages/_app.tsx', 'app/layout.tsx', 'src/App.tsx', 'index.html'];
293
+ for (const p of priorities) {
294
+ if (validSet.has(p)) { entries.push(p); break; }
295
+ }
296
+ } else if (projectType.includes('Java')) {
297
+ const applicationJava = validFiles.filter(f => f.endsWith('Application.java'));
298
+ if (applicationJava.length > 0) {
299
+ entries.push(...applicationJava);
300
+ } else {
301
+ for (const f of validFiles) {
302
+ if (f.endsWith('.java')) {
303
+ try {
304
+ const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
305
+ if (content.includes('public static void main')) {
306
+ entries.push(f);
307
+ break;
308
+ }
309
+ } catch {}
310
+ }
311
+ }
312
+ }
313
+ } else if (projectType.includes('Python')) {
314
+ const priorities = ['main.py', 'app.py', '__main__.py'];
315
+ for (const p of priorities) {
316
+ if (validSet.has(p)) { entries.push(p); break; }
317
+ }
318
+ if (entries.length === 0) {
319
+ for (const f of validFiles) {
320
+ if (f.endsWith('.py')) {
321
+ try {
322
+ const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
323
+ if (content.includes('if __name__ == "__main__":') || content.includes("if __name__ == '__main__':")) {
324
+ entries.push(f);
325
+ break;
326
+ }
327
+ } catch {}
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ if (entries.length === 0) {
334
+ // Fallbacks for Unknown or missed projects
335
+ let best = null;
336
+ let maxScore = -1;
337
+ for (const f of validFiles) {
338
+ if (/^(src\/)?index\.[a-z]+$/.test(f)) {
339
+ entries.push(f);
340
+ break;
341
+ }
342
+ }
343
+ if (entries.length === 0) {
344
+ for (const f of validFiles) {
345
+ if (f.match(/\.(js|ts|jsx|tsx|py|java|go|rb|php)$/)) {
346
+ try {
347
+ const content = fs.readFileSync(path.join(rootPath, f), 'utf8');
348
+ const score = (content.match(/import /g) || []).length +
349
+ (content.match(/export /g) || []).length +
350
+ (content.match(/require\(/g) || []).length;
351
+ if (score > maxScore) {
352
+ maxScore = score;
353
+ best = f;
354
+ }
355
+ } catch {}
356
+ }
357
+ }
358
+ if (best) entries.push(best);
359
+ }
360
+ }
361
+
362
+ return Array.from(new Set(entries));
363
+ }
364
+
258
365
  // ---------------------------------------------------------------------------
259
366
  // Tree helpers
260
367
  // ---------------------------------------------------------------------------
@@ -279,8 +386,9 @@ function countFolders(node) {
279
386
  // Public API
280
387
  // ---------------------------------------------------------------------------
281
388
 
282
- export function scan(targetPath) {
389
+ export function scan(targetPath, options = {}) {
283
390
  const rootPath = path.resolve(targetPath);
391
+ const includeHidden = !!options.includeHidden;
284
392
 
285
393
  // Validate target
286
394
  let stat;
@@ -293,7 +401,7 @@ export function scan(targetPath) {
293
401
  /** @type {Array<string>} */
294
402
  const flatFiles = [];
295
403
  /** @type {Array<string>} */
296
- const entryPoints = [];
404
+ let entryPoints = [];
297
405
 
298
406
  const startTime = performance.now();
299
407
  let tree;
@@ -301,12 +409,13 @@ export function scan(targetPath) {
301
409
  let totalFolders = 0;
302
410
 
303
411
  if (stat.isDirectory()) {
304
- tree = walkDirectory(rootPath, rootPath, flatFiles, entryPoints);
412
+ tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden);
305
413
  projectType = detectProjectType(rootPath);
306
414
  // Count all directory nodes in the tree (excluding root itself).
307
415
  totalFolders = countFolders(tree) - 1;
416
+ entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
308
417
  } else if (stat.isFile()) {
309
- const relFilePath = path.basename(rootPath);
418
+ const relFilePath = toPosix(path.basename(rootPath));
310
419
  tree = {
311
420
  type: 'directory',
312
421
  name: path.basename(path.dirname(rootPath)),
@@ -320,9 +429,7 @@ export function scan(targetPath) {
320
429
  }]
321
430
  };
322
431
  flatFiles.push(relFilePath);
323
- if (isEntryPoint(relFilePath)) {
324
- entryPoints.push(relFilePath);
325
- }
432
+ entryPoints = [relFilePath]; // A single file is its own entry point
326
433
  } else {
327
434
  throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
328
435
  }