@lakindu_perera/toren 1.0.4 → 1.0.7
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 +81 -44
- package/bin/toren.js +65 -44
- package/package.json +2 -1
- package/src/detectors/config-detector.js +81 -0
- package/src/detectors/script-detector.js +32 -0
- package/src/focused-output.js +99 -24
- package/src/renderers/console-renderer.js +136 -44
- package/src/renderers/html-renderer.js +158 -62
- package/src/renderers/json-renderer.js +119 -19
- package/src/renderers/markdown-renderer.js +96 -87
- package/src/scanner/scan.js +37 -4
|
@@ -1,53 +1,153 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Toren — JSON Renderer
|
|
3
3
|
*
|
|
4
|
-
* Consumes a ScanResult and produces a
|
|
4
|
+
* Consumes a ScanResult and produces a stable, consistently-ordered JSON
|
|
5
|
+
* object suitable for programmatic consumption.
|
|
6
|
+
*
|
|
7
|
+
* Design contract (mirrors all other renderers):
|
|
8
|
+
* - Accepts a ScanResult and an optional options object.
|
|
9
|
+
* - Never scans files or modifies the data it receives.
|
|
10
|
+
* - All arrays are always present — never undefined or missing.
|
|
11
|
+
* - Property order is fixed and documented below.
|
|
12
|
+
* - Backward compatible: no existing field is removed or renamed.
|
|
13
|
+
*
|
|
14
|
+
* Output property order:
|
|
15
|
+
* 1. meta — schema version + generator provenance
|
|
16
|
+
* 2. project — name (basename), path (relative), detected type
|
|
17
|
+
* 3. frameworks — derived array of detected frameworks ([] when none)
|
|
18
|
+
* 4. entryPoints — array of detected entry-point paths
|
|
19
|
+
* 5. configs — array of detected configuration file paths
|
|
20
|
+
* 6. scripts — array of { name, command } objects
|
|
21
|
+
* 7. statistics — file/folder counts and scan duration (structured)
|
|
22
|
+
* 8. structure — recursive file-tree array
|
|
23
|
+
* 9. summary — retained for backward compatibility (same data as statistics)
|
|
5
24
|
*/
|
|
6
25
|
|
|
7
26
|
import path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const pkg = require('../../package.json');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tree mapper
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
8
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Recursively map an internal DirNode / FileNode to a clean JSON shape.
|
|
38
|
+
* Directory nodes always include a `children` array (never undefined).
|
|
39
|
+
*
|
|
40
|
+
* @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
|
|
41
|
+
* @returns {{ type: 'folder'|'file', name: string, children?: object[] }}
|
|
42
|
+
*/
|
|
9
43
|
function mapTree(node) {
|
|
10
44
|
if (node.type === 'directory') {
|
|
11
45
|
return {
|
|
12
|
-
type:
|
|
13
|
-
name:
|
|
14
|
-
children: (node.children || []).map(mapTree)
|
|
15
|
-
};
|
|
16
|
-
} else {
|
|
17
|
-
return {
|
|
18
|
-
type: 'file',
|
|
19
|
-
name: node.name
|
|
46
|
+
type: 'folder',
|
|
47
|
+
name: node.name,
|
|
48
|
+
children: (node.children || []).map(mapTree),
|
|
20
49
|
};
|
|
21
50
|
}
|
|
51
|
+
return {
|
|
52
|
+
type: 'file',
|
|
53
|
+
name: node.name,
|
|
54
|
+
};
|
|
22
55
|
}
|
|
23
56
|
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Helpers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive a `frameworks` array from the scanner's `projectType` string.
|
|
63
|
+
* Returns a single-element array when a framework is detected, empty otherwise.
|
|
64
|
+
* This is a pure presentation decision — no business logic is added.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectType
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function deriveFrameworks(projectType) {
|
|
70
|
+
if (!projectType || projectType === 'Unknown') return [];
|
|
71
|
+
return [projectType];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Public API
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a ScanResult as a stable JSON document to stdout.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
82
|
+
* @param {{ cwd?: string }} [options]
|
|
83
|
+
*/
|
|
24
84
|
export function render(result, options = {}) {
|
|
25
85
|
const {
|
|
26
86
|
rootPath,
|
|
27
87
|
projectType,
|
|
28
|
-
entryPoints,
|
|
88
|
+
entryPoints = [],
|
|
89
|
+
configs = [],
|
|
90
|
+
scripts = [],
|
|
29
91
|
tree,
|
|
30
|
-
flatFiles,
|
|
31
|
-
totalFolders,
|
|
92
|
+
flatFiles = [],
|
|
93
|
+
totalFolders = 0,
|
|
32
94
|
scanDurationMs,
|
|
33
95
|
} = result;
|
|
34
96
|
|
|
35
|
-
const cwd
|
|
36
|
-
const relRoot
|
|
97
|
+
const cwd = options.cwd ?? process.cwd();
|
|
98
|
+
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
99
|
+
const rootName = path.basename(rootPath) || relRoot;
|
|
100
|
+
|
|
101
|
+
// Shared statistics values — computed once, used in both `statistics` and
|
|
102
|
+
// the backward-compatible `summary` block.
|
|
103
|
+
// Guard against NaN/undefined: JSON.stringify(NaN) produces null, breaking
|
|
104
|
+
// the schema guarantee that durationMs is always a number.
|
|
105
|
+
const totalFiles = flatFiles.length;
|
|
106
|
+
const durationMs = Number.isFinite(scanDurationMs) ? Math.round(scanDurationMs) : 0;
|
|
37
107
|
|
|
38
108
|
const output = {
|
|
109
|
+
// 1. Provenance — lets consumers detect schema changes.
|
|
110
|
+
meta: {
|
|
111
|
+
generatedBy: 'Toren',
|
|
112
|
+
version: pkg.version,
|
|
113
|
+
schema: 1,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// 2. Project identity — name is the human-readable basename; path is the
|
|
117
|
+
// relative path used for filesystem resolution.
|
|
39
118
|
project: {
|
|
119
|
+
name: rootName,
|
|
40
120
|
path: relRoot,
|
|
41
121
|
type: projectType,
|
|
42
|
-
framework: projectType,
|
|
43
122
|
},
|
|
123
|
+
|
|
124
|
+
// 3. Detected frameworks — always an array.
|
|
125
|
+
frameworks: deriveFrameworks(projectType),
|
|
126
|
+
|
|
127
|
+
// 4–6. Discovery results — all arrays, always present, never null.
|
|
128
|
+
entryPoints: Array.isArray(entryPoints) ? entryPoints : [],
|
|
129
|
+
configs: Array.isArray(configs) ? configs : [],
|
|
130
|
+
scripts: Array.isArray(scripts) ? scripts : [],
|
|
131
|
+
|
|
132
|
+
// 7. Structured statistics — always a complete object with numeric values.
|
|
133
|
+
// durationMs is rounded to the nearest millisecond (integer).
|
|
134
|
+
statistics: {
|
|
135
|
+
files: totalFiles,
|
|
136
|
+
folders: totalFolders,
|
|
137
|
+
durationMs,
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// 8. File-tree — array of root-level nodes; always present.
|
|
141
|
+
// Empty array when the scanned directory is empty.
|
|
142
|
+
structure: (tree?.children || []).map(mapTree),
|
|
143
|
+
|
|
144
|
+
// 9. Backward-compatible summary block — preserved for existing consumers.
|
|
145
|
+
// Contains the same data under the original field names.
|
|
44
146
|
summary: {
|
|
45
|
-
totalFiles
|
|
147
|
+
totalFiles,
|
|
46
148
|
totalFolders,
|
|
47
|
-
scanDurationMs:
|
|
149
|
+
scanDurationMs: durationMs,
|
|
48
150
|
},
|
|
49
|
-
entryPoints,
|
|
50
|
-
structure: (tree.children || []).map(mapTree)
|
|
51
151
|
};
|
|
52
152
|
|
|
53
153
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -28,37 +28,7 @@ import path from 'node:path';
|
|
|
28
28
|
// Plain-text tree builder (Markdown-safe, no ANSI, no emoji)
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* Build an in-memory nested tree from a flat list of relative file paths.
|
|
33
|
-
* This avoids repeating the walk already done by the scanner while keeping
|
|
34
|
-
* the markdown renderer completely self-contained.
|
|
35
|
-
*
|
|
36
|
-
* @param {string[]} flatFiles - Relative file paths produced by scan()
|
|
37
|
-
* @returns {{ name: string, type: 'directory'|'file', children: object }[]}
|
|
38
|
-
*/
|
|
39
|
-
function buildTree(flatFiles) {
|
|
40
|
-
/** @type {{ type: string, children: Record<string, object> }} */
|
|
41
|
-
const root = { type: 'directory', children: {} };
|
|
42
|
-
|
|
43
|
-
for (const filePath of flatFiles) {
|
|
44
|
-
const parts = filePath.split(/[/\\]/).filter(Boolean);
|
|
45
|
-
let node = root;
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < parts.length; i++) {
|
|
48
|
-
const part = parts[i];
|
|
49
|
-
const isLeaf = i === parts.length - 1;
|
|
50
|
-
|
|
51
|
-
if (!node.children[part]) {
|
|
52
|
-
node.children[part] = isLeaf
|
|
53
|
-
? { type: 'file', name: part }
|
|
54
|
-
: { type: 'directory', name: part, children: {} };
|
|
55
|
-
}
|
|
56
|
-
node = node.children[part];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
31
|
|
|
60
|
-
return root;
|
|
61
|
-
}
|
|
62
32
|
|
|
63
33
|
/**
|
|
64
34
|
* Recursively serialise a tree node into classic tree-connector lines.
|
|
@@ -81,11 +51,7 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
|
|
|
81
51
|
lines.push(`${prefix}${connector}${label}`);
|
|
82
52
|
|
|
83
53
|
if (node.type === 'directory') {
|
|
84
|
-
const children =
|
|
85
|
-
// Directories before files, then alphabetically.
|
|
86
|
-
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
87
|
-
return a.name.localeCompare(b.name);
|
|
88
|
-
});
|
|
54
|
+
const children = node.children || [];
|
|
89
55
|
|
|
90
56
|
// Truncate deep directories with an ellipsis rather than cutting silently.
|
|
91
57
|
if (depth === maxDepth - 1 && children.length > 0) {
|
|
@@ -107,21 +73,18 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
|
|
|
107
73
|
}
|
|
108
74
|
|
|
109
75
|
/**
|
|
110
|
-
* Convert a
|
|
76
|
+
* Convert a ScanResult tree into a Markdown-safe tree string.
|
|
111
77
|
*
|
|
112
|
-
* @param {
|
|
78
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
113
79
|
* @param {string} rootName - Display name for the root node (e.g. "my-app/")
|
|
80
|
+
* @param {number} totalFiles
|
|
114
81
|
* @returns {string}
|
|
115
82
|
*/
|
|
116
|
-
function buildTreeString(
|
|
117
|
-
if (
|
|
83
|
+
function buildTreeString(tree, rootName, totalFiles) {
|
|
84
|
+
if (totalFiles === 0) return '';
|
|
118
85
|
|
|
119
|
-
const root = buildTree(flatFiles);
|
|
120
86
|
const lines = [`${rootName}/`];
|
|
121
|
-
const children =
|
|
122
|
-
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
123
|
-
return a.name.localeCompare(b.name);
|
|
124
|
-
});
|
|
87
|
+
const children = tree.children || [];
|
|
125
88
|
|
|
126
89
|
for (let i = 0; i < children.length; i++) {
|
|
127
90
|
serializeNode(children[i], '', i === children.length - 1, lines);
|
|
@@ -165,6 +128,19 @@ function markdownTable(rows) {
|
|
|
165
128
|
return [head, hr, ...body].join('\n');
|
|
166
129
|
}
|
|
167
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Derive a frameworks array from the projectType string.
|
|
133
|
+
* Returns [] when no specific framework is detected.
|
|
134
|
+
* Mirrors the identical derivation in console-renderer.js and json-renderer.js.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} projectType
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
function deriveFrameworks(projectType) {
|
|
140
|
+
if (!projectType || projectType === 'Unknown') return [];
|
|
141
|
+
return [projectType];
|
|
142
|
+
}
|
|
143
|
+
|
|
168
144
|
/**
|
|
169
145
|
* Build a right-aligned Markdown table (used for statistics).
|
|
170
146
|
*
|
|
@@ -190,11 +166,9 @@ function statsTable(rows) {
|
|
|
190
166
|
|
|
191
167
|
/** @param {string[]} out */
|
|
192
168
|
function sectionTitle(out) {
|
|
193
|
-
out.push('#
|
|
169
|
+
out.push('# Toren Report');
|
|
194
170
|
out.push('');
|
|
195
171
|
out.push('Generated by **Toren** — Codebase Onboarding Intelligence');
|
|
196
|
-
out.push('');
|
|
197
|
-
out.push('---');
|
|
198
172
|
}
|
|
199
173
|
|
|
200
174
|
/**
|
|
@@ -206,7 +180,7 @@ function sectionSummary(out, result, relRoot) {
|
|
|
206
180
|
const { projectType, flatFiles, totalFolders } = result;
|
|
207
181
|
|
|
208
182
|
out.push('');
|
|
209
|
-
out.push('## Project
|
|
183
|
+
out.push('## Project');
|
|
210
184
|
out.push('');
|
|
211
185
|
out.push(markdownTable([
|
|
212
186
|
['Project Type', projectType],
|
|
@@ -214,52 +188,100 @@ function sectionSummary(out, result, relRoot) {
|
|
|
214
188
|
['Total Files', String(flatFiles.length)],
|
|
215
189
|
['Total Folders', String(totalFolders)],
|
|
216
190
|
]));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string[]} out
|
|
195
|
+
* @param {string} projectType
|
|
196
|
+
*/
|
|
197
|
+
function sectionFrameworks(out, projectType) {
|
|
198
|
+
const frameworks = deriveFrameworks(projectType);
|
|
199
|
+
|
|
217
200
|
out.push('');
|
|
218
|
-
out.push('
|
|
201
|
+
out.push('## Frameworks');
|
|
202
|
+
out.push('');
|
|
203
|
+
|
|
204
|
+
if (frameworks.length === 0) {
|
|
205
|
+
out.push('No frameworks detected.');
|
|
206
|
+
} else {
|
|
207
|
+
for (const fw of frameworks) {
|
|
208
|
+
out.push(`- ${fw}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
219
211
|
}
|
|
220
212
|
|
|
221
213
|
/**
|
|
222
214
|
* @param {string[]} out
|
|
223
|
-
* @param {string[]}
|
|
215
|
+
* @param {string[]} configs
|
|
224
216
|
*/
|
|
225
|
-
function
|
|
217
|
+
function sectionConfigurationFiles(out, configs) {
|
|
226
218
|
out.push('');
|
|
227
|
-
out.push('##
|
|
219
|
+
out.push('## Configurations');
|
|
228
220
|
out.push('');
|
|
229
221
|
|
|
230
|
-
if (
|
|
231
|
-
out.push('No
|
|
222
|
+
if (configs.length === 0) {
|
|
223
|
+
out.push('No configuration files detected.');
|
|
232
224
|
} else {
|
|
233
|
-
for (const
|
|
234
|
-
out.push(`-
|
|
225
|
+
for (const c of configs) {
|
|
226
|
+
out.push(`- ${c}`);
|
|
235
227
|
}
|
|
236
228
|
}
|
|
229
|
+
}
|
|
237
230
|
|
|
231
|
+
/**
|
|
232
|
+
* @param {string[]} out
|
|
233
|
+
* @param {Array<{name: string, command: string}>} scripts
|
|
234
|
+
*/
|
|
235
|
+
function sectionPackageScripts(out, scripts) {
|
|
238
236
|
out.push('');
|
|
239
|
-
out.push('
|
|
237
|
+
out.push('## Scripts');
|
|
238
|
+
out.push('');
|
|
239
|
+
|
|
240
|
+
if (scripts.length === 0) {
|
|
241
|
+
out.push('No package scripts detected.');
|
|
242
|
+
} else {
|
|
243
|
+
for (const s of scripts) {
|
|
244
|
+
out.push(`- ${s.name}: ${s.command}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
/**
|
|
243
250
|
* @param {string[]} out
|
|
244
|
-
* @param {string[]}
|
|
245
|
-
* @param {string} rootName
|
|
251
|
+
* @param {string[]} entryPoints
|
|
246
252
|
*/
|
|
247
|
-
function
|
|
253
|
+
function sectionEntryPoints(out, entryPoints) {
|
|
248
254
|
out.push('');
|
|
249
|
-
out.push('##
|
|
255
|
+
out.push('## Entry Points');
|
|
250
256
|
out.push('');
|
|
251
257
|
|
|
252
|
-
if (
|
|
253
|
-
out.push('No
|
|
258
|
+
if (entryPoints.length === 0) {
|
|
259
|
+
out.push('No entry points detected.');
|
|
254
260
|
} else {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
out.push('```');
|
|
261
|
+
for (const ep of entryPoints) {
|
|
262
|
+
out.push(`- ${ep}`);
|
|
263
|
+
}
|
|
259
264
|
}
|
|
265
|
+
}
|
|
260
266
|
|
|
267
|
+
/**
|
|
268
|
+
* @param {string[]} out
|
|
269
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
270
|
+
* @param {number} totalFiles
|
|
271
|
+
* @param {string} rootName
|
|
272
|
+
*/
|
|
273
|
+
function sectionFolderStructure(out, tree, totalFiles, rootName) {
|
|
274
|
+
// Contract: omit section entirely when no files were scanned.
|
|
275
|
+
if (totalFiles === 0) return;
|
|
276
|
+
|
|
277
|
+
out.push('');
|
|
278
|
+
out.push('## Structure');
|
|
261
279
|
out.push('');
|
|
262
|
-
|
|
280
|
+
|
|
281
|
+
const treeStr = buildTreeString(tree, rootName, totalFiles);
|
|
282
|
+
out.push('```text');
|
|
283
|
+
out.push(treeStr);
|
|
284
|
+
out.push('```');
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
/**
|
|
@@ -277,21 +299,6 @@ function sectionStatistics(out, result) {
|
|
|
277
299
|
['Folders', totalFolders],
|
|
278
300
|
['Scan Duration', formatDuration(scanDurationMs)],
|
|
279
301
|
]));
|
|
280
|
-
out.push('');
|
|
281
|
-
out.push('---');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @param {string[]} out
|
|
286
|
-
*/
|
|
287
|
-
function sectionScanInfo(out) {
|
|
288
|
-
out.push('');
|
|
289
|
-
out.push('## Scan Information');
|
|
290
|
-
out.push('');
|
|
291
|
-
out.push('Generated by **Toren**');
|
|
292
|
-
out.push('');
|
|
293
|
-
out.push('Output Format: Markdown');
|
|
294
|
-
out.push('');
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
// ---------------------------------------------------------------------------
|
|
@@ -308,21 +315,23 @@ function sectionScanInfo(out) {
|
|
|
308
315
|
* @param {{ cwd?: string }} [options]
|
|
309
316
|
*/
|
|
310
317
|
export function render(result, options = {}) {
|
|
311
|
-
const { rootPath, entryPoints, flatFiles } = result;
|
|
318
|
+
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
|
|
312
319
|
|
|
313
320
|
const cwd = options.cwd ?? process.cwd();
|
|
314
321
|
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
315
|
-
const rootName = path.basename(rootPath)
|
|
322
|
+
const rootName = relRoot === '.' ? path.basename(rootPath) : relRoot;
|
|
316
323
|
|
|
317
324
|
/** @type {string[]} */
|
|
318
325
|
const out = [];
|
|
319
326
|
|
|
320
327
|
sectionTitle(out);
|
|
321
328
|
sectionSummary(out, result, relRoot);
|
|
329
|
+
sectionFrameworks(out, projectType);
|
|
322
330
|
sectionEntryPoints(out, entryPoints);
|
|
323
|
-
|
|
331
|
+
sectionConfigurationFiles(out, configs);
|
|
332
|
+
sectionPackageScripts(out, scripts);
|
|
324
333
|
sectionStatistics(out, result);
|
|
325
|
-
|
|
334
|
+
sectionFolderStructure(out, tree, flatFiles.length, rootName);
|
|
326
335
|
|
|
327
336
|
console.log(out.join('\n'));
|
|
328
337
|
}
|
package/src/scanner/scan.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
|
+
import { detectConfigs } from '../detectors/config-detector.js';
|
|
24
|
+
import { detectScripts } from '../detectors/script-detector.js';
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// Constants
|
|
@@ -96,6 +98,8 @@ const PROJECT_TYPE_MARKERS = [
|
|
|
96
98
|
* @property {string} rootPath - Absolute path that was scanned
|
|
97
99
|
* @property {string} projectType - Detected project type label
|
|
98
100
|
* @property {Array<string>} entryPoints - Relative paths of detected entry points
|
|
101
|
+
* @property {Array<string>} configs - Relative paths of detected config files
|
|
102
|
+
* @property {Array<{name: string, command: string}>} scripts - Parsed package scripts
|
|
99
103
|
* @property {DirNode} tree - Full in-memory file tree
|
|
100
104
|
* @property {Array<string>} flatFiles - All relative file paths (flat list)
|
|
101
105
|
* @property {number} totalFolders - Total number of directories walked
|
|
@@ -182,7 +186,11 @@ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
|
|
|
182
186
|
node.children.push(childNode);
|
|
183
187
|
} else if (dirent.isFile()) {
|
|
184
188
|
if (flatFiles.length >= maxFiles) {
|
|
185
|
-
|
|
189
|
+
const err = new Error(`Max file scan limit exceeded (${maxFiles} files).`);
|
|
190
|
+
err.title = 'Scan limit exceeded';
|
|
191
|
+
err.detailLabel = 'Hint';
|
|
192
|
+
err.detailValue = 'Use --max-files <number> to increase the limit.';
|
|
193
|
+
throw err;
|
|
186
194
|
}
|
|
187
195
|
const relFilePath = toPosix(path.relative(rootPath, childPath));
|
|
188
196
|
|
|
@@ -445,14 +453,28 @@ export function scan(targetPath, options = {}) {
|
|
|
445
453
|
let stat;
|
|
446
454
|
try {
|
|
447
455
|
stat = fs.statSync(rootPath);
|
|
448
|
-
} catch {
|
|
449
|
-
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const error = new Error(`Path does not exist: ${rootPath}`);
|
|
458
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
459
|
+
error.title = 'Permission denied';
|
|
460
|
+
error.message = 'You do not have permission to access the specified path.';
|
|
461
|
+
} else {
|
|
462
|
+
error.title = 'Invalid directory';
|
|
463
|
+
error.message = 'The specified path does not exist.';
|
|
464
|
+
}
|
|
465
|
+
error.detailLabel = 'Path';
|
|
466
|
+
error.detailValue = rootPath;
|
|
467
|
+
throw error;
|
|
450
468
|
}
|
|
451
469
|
|
|
452
470
|
/** @type {Array<string>} */
|
|
453
471
|
const flatFiles = [];
|
|
454
472
|
/** @type {Array<string>} */
|
|
455
473
|
let entryPoints = [];
|
|
474
|
+
/** @type {Array<string>} */
|
|
475
|
+
let configs = [];
|
|
476
|
+
/** @type {Array<{name: string, command: string}>} */
|
|
477
|
+
let scripts = [];
|
|
456
478
|
|
|
457
479
|
const startTime = performance.now();
|
|
458
480
|
let tree;
|
|
@@ -465,6 +487,8 @@ export function scan(targetPath, options = {}) {
|
|
|
465
487
|
// Count all directory nodes in the tree (excluding root itself).
|
|
466
488
|
totalFolders = countFolders(tree) - 1;
|
|
467
489
|
entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
|
|
490
|
+
configs = detectConfigs(flatFiles).configs;
|
|
491
|
+
scripts = detectScripts(rootPath).scripts;
|
|
468
492
|
} else if (stat.isFile()) {
|
|
469
493
|
const relFilePath = toPosix(path.basename(rootPath));
|
|
470
494
|
tree = {
|
|
@@ -481,8 +505,15 @@ export function scan(targetPath, options = {}) {
|
|
|
481
505
|
};
|
|
482
506
|
flatFiles.push(relFilePath);
|
|
483
507
|
entryPoints = [relFilePath]; // A single file is its own entry point
|
|
508
|
+
configs = detectConfigs(flatFiles).configs;
|
|
509
|
+
scripts = detectScripts(path.dirname(rootPath)).scripts;
|
|
484
510
|
} else {
|
|
485
|
-
|
|
511
|
+
const err = new Error(`Path is neither a file nor a directory: ${rootPath}`);
|
|
512
|
+
err.title = 'Unsupported path type';
|
|
513
|
+
err.message = 'The specified path is neither a file nor a directory.';
|
|
514
|
+
err.detailLabel = 'Path';
|
|
515
|
+
err.detailValue = rootPath;
|
|
516
|
+
throw err;
|
|
486
517
|
}
|
|
487
518
|
|
|
488
519
|
const scanDurationMs = performance.now() - startTime;
|
|
@@ -491,6 +522,8 @@ export function scan(targetPath, options = {}) {
|
|
|
491
522
|
rootPath,
|
|
492
523
|
projectType,
|
|
493
524
|
entryPoints,
|
|
525
|
+
configs,
|
|
526
|
+
scripts,
|
|
494
527
|
tree,
|
|
495
528
|
flatFiles,
|
|
496
529
|
totalFolders,
|