codesummary 1.0.0
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/LICENSE +674 -0
- package/README.md +370 -0
- package/RELEASE.md +412 -0
- package/bin/codesummary.js +13 -0
- package/features.md +502 -0
- package/package.json +84 -0
- package/src/cli.js +392 -0
- package/src/configManager.js +427 -0
- package/src/errorHandler.js +343 -0
- package/src/index.js +26 -0
- package/src/pdfGenerator.js +427 -0
- package/src/scanner.js +330 -0
package/src/scanner.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ErrorHandler from './errorHandler.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File Scanner for CodeSummary
|
|
8
|
+
* Handles recursive directory traversal and file filtering
|
|
9
|
+
*/
|
|
10
|
+
export class Scanner {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.allowedExtensions = new Set(config.allowedExtensions.map(ext => ext.toLowerCase()));
|
|
14
|
+
this.excludeDirs = new Set(config.excludeDirs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scan a directory recursively and return files grouped by extension
|
|
19
|
+
* @param {string} rootPath - Root directory to scan
|
|
20
|
+
* @returns {Promise<object>} Object with extensions as keys and file arrays as values
|
|
21
|
+
*/
|
|
22
|
+
async scanDirectory(rootPath) {
|
|
23
|
+
try {
|
|
24
|
+
// Validate and sanitize input path
|
|
25
|
+
const sanitizedPath = ErrorHandler.sanitizeInput(rootPath);
|
|
26
|
+
ErrorHandler.validatePath(sanitizedPath, { mustExist: true, preventTraversal: true });
|
|
27
|
+
|
|
28
|
+
const resolvedRoot = path.resolve(sanitizedPath);
|
|
29
|
+
const stats = await fs.stat(resolvedRoot);
|
|
30
|
+
|
|
31
|
+
if (!stats.isDirectory()) {
|
|
32
|
+
throw new Error(`Path is not a directory: ${resolvedRoot}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(chalk.gray(`Scanning directory: ${resolvedRoot}`));
|
|
36
|
+
|
|
37
|
+
const filesByExtension = {};
|
|
38
|
+
const scannedFiles = new Set(); // Prevent duplicates
|
|
39
|
+
|
|
40
|
+
await this.walkDirectory(resolvedRoot, resolvedRoot, filesByExtension, scannedFiles);
|
|
41
|
+
|
|
42
|
+
// Sort files within each extension group
|
|
43
|
+
Object.keys(filesByExtension).forEach(ext => {
|
|
44
|
+
filesByExtension[ext].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return filesByExtension;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') {
|
|
50
|
+
throw new Error(`Directory does not exist: ${rootPath}`);
|
|
51
|
+
} else if (error.code === 'EACCES') {
|
|
52
|
+
throw new Error(`Permission denied accessing directory: ${rootPath}`);
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively walk through directory structure
|
|
60
|
+
* @param {string} currentPath - Current directory being processed
|
|
61
|
+
* @param {string} rootPath - Original root path for relative path calculation
|
|
62
|
+
* @param {object} filesByExtension - Accumulator object for results
|
|
63
|
+
* @param {Set} scannedFiles - Set to track processed files and avoid duplicates
|
|
64
|
+
*/
|
|
65
|
+
async walkDirectory(currentPath, rootPath, filesByExtension, scannedFiles) {
|
|
66
|
+
try {
|
|
67
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
71
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
72
|
+
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
// Skip excluded directories and hidden directories (unless explicitly allowed)
|
|
75
|
+
if (this.shouldSkipDirectory(entry.name, relativePath)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Recursively scan subdirectory
|
|
80
|
+
await this.walkDirectory(fullPath, rootPath, filesByExtension, scannedFiles);
|
|
81
|
+
} else if (entry.isFile()) {
|
|
82
|
+
// Process file if it matches criteria
|
|
83
|
+
await this.processFile(fullPath, rootPath, filesByExtension, scannedFiles);
|
|
84
|
+
}
|
|
85
|
+
// Skip symbolic links, special files, etc.
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Handle different types of file system errors appropriately
|
|
89
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
90
|
+
console.warn(chalk.yellow(`WARNING: Permission denied: ${currentPath}`));
|
|
91
|
+
} else if (error.code === 'ENOENT') {
|
|
92
|
+
console.warn(chalk.yellow(`WARNING: Directory no longer exists: ${currentPath}`));
|
|
93
|
+
} else {
|
|
94
|
+
console.warn(chalk.yellow(`WARNING: Cannot read directory ${currentPath}: ${error.message}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Process a single file and add it to results if it matches criteria
|
|
101
|
+
* @param {string} fullPath - Absolute path to the file
|
|
102
|
+
* @param {string} rootPath - Root path for relative calculation
|
|
103
|
+
* @param {object} filesByExtension - Results accumulator
|
|
104
|
+
* @param {Set} scannedFiles - Set of already processed files
|
|
105
|
+
*/
|
|
106
|
+
async processFile(fullPath, rootPath, filesByExtension, scannedFiles) {
|
|
107
|
+
try {
|
|
108
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
109
|
+
|
|
110
|
+
// Avoid processing the same file twice (in case of symlinks)
|
|
111
|
+
if (scannedFiles.has(fullPath)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
scannedFiles.add(fullPath);
|
|
115
|
+
|
|
116
|
+
const extension = path.extname(relativePath).toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Skip files without extensions or not in allowed list
|
|
119
|
+
if (!extension || !this.allowedExtensions.has(extension)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Skip hidden files (starting with .) unless explicitly needed
|
|
124
|
+
const fileName = path.basename(relativePath);
|
|
125
|
+
if (fileName.startsWith('.') && !this.isAllowedHiddenFile(fileName)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Verify file is readable
|
|
130
|
+
const stats = await fs.stat(fullPath);
|
|
131
|
+
if (!stats.isFile()) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add to results
|
|
136
|
+
if (!filesByExtension[extension]) {
|
|
137
|
+
filesByExtension[extension] = [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
filesByExtension[extension].push({
|
|
141
|
+
relativePath: relativePath.replace(/\\/g, '/'), // Normalize path separators
|
|
142
|
+
absolutePath: fullPath,
|
|
143
|
+
size: stats.size,
|
|
144
|
+
modified: stats.mtime
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Handle file processing errors with appropriate context
|
|
149
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
150
|
+
console.warn(chalk.yellow(`WARNING: Permission denied: ${relativePath}`));
|
|
151
|
+
} else if (error.code === 'ENOENT') {
|
|
152
|
+
// File might have been deleted during scan
|
|
153
|
+
console.warn(chalk.yellow(`WARNING: File no longer exists: ${relativePath}`));
|
|
154
|
+
} else {
|
|
155
|
+
console.warn(chalk.yellow(`WARNING: Cannot process file ${relativePath}: ${error.message}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Determine if a directory should be skipped
|
|
162
|
+
* @param {string} dirName - Directory name
|
|
163
|
+
* @param {string} relativePath - Relative path from root
|
|
164
|
+
* @returns {boolean} True if directory should be skipped
|
|
165
|
+
*/
|
|
166
|
+
shouldSkipDirectory(dirName, relativePath) {
|
|
167
|
+
// Skip directories in exclude list
|
|
168
|
+
if (this.excludeDirs.has(dirName)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Skip hidden directories (starting with .) unless explicitly allowed
|
|
173
|
+
if (dirName.startsWith('.') && !this.isAllowedHiddenDirectory(dirName)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Skip common build/cache directories that might not be in exclude list
|
|
178
|
+
const commonSkipDirs = new Set([
|
|
179
|
+
'tmp', 'temp', 'cache', '.cache', 'logs', '.logs',
|
|
180
|
+
'bower_components', 'vendor', '.vendor'
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (commonSkipDirs.has(dirName.toLowerCase())) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a hidden file should be included
|
|
192
|
+
* @param {string} fileName - File name
|
|
193
|
+
* @returns {boolean} True if file should be included
|
|
194
|
+
*/
|
|
195
|
+
isAllowedHiddenFile(fileName) {
|
|
196
|
+
const allowedHiddenFiles = new Set([
|
|
197
|
+
'.gitignore', '.gitattributes', '.editorconfig',
|
|
198
|
+
'.eslintrc.js', '.eslintrc.json', '.prettierrc',
|
|
199
|
+
'.env.example', '.htaccess'
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
return allowedHiddenFiles.has(fileName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if a hidden directory should be included
|
|
207
|
+
* @param {string} dirName - Directory name
|
|
208
|
+
* @returns {boolean} True if directory should be included
|
|
209
|
+
*/
|
|
210
|
+
isAllowedHiddenDirectory(dirName) {
|
|
211
|
+
const allowedHiddenDirs = new Set([
|
|
212
|
+
'.github', '.gitlab', '.circleci'
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
return allowedHiddenDirs.has(dirName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get file extension descriptions for user display
|
|
220
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
221
|
+
* @returns {Array} Array of extension info objects
|
|
222
|
+
*/
|
|
223
|
+
getExtensionInfo(filesByExtension) {
|
|
224
|
+
const extensionDescriptions = {
|
|
225
|
+
'.js': 'JavaScript',
|
|
226
|
+
'.ts': 'TypeScript',
|
|
227
|
+
'.jsx': 'React JSX',
|
|
228
|
+
'.tsx': 'TypeScript JSX',
|
|
229
|
+
'.json': 'JSON',
|
|
230
|
+
'.xml': 'XML',
|
|
231
|
+
'.html': 'HTML',
|
|
232
|
+
'.css': 'CSS',
|
|
233
|
+
'.scss': 'SCSS',
|
|
234
|
+
'.sass': 'Sass',
|
|
235
|
+
'.md': 'Markdown',
|
|
236
|
+
'.txt': 'Text',
|
|
237
|
+
'.py': 'Python',
|
|
238
|
+
'.java': 'Java',
|
|
239
|
+
'.cs': 'C#',
|
|
240
|
+
'.cpp': 'C++',
|
|
241
|
+
'.c': 'C',
|
|
242
|
+
'.h': 'Header',
|
|
243
|
+
'.yaml': 'YAML',
|
|
244
|
+
'.yml': 'YAML',
|
|
245
|
+
'.sh': 'Shell Script',
|
|
246
|
+
'.bat': 'Batch File',
|
|
247
|
+
'.ps1': 'PowerShell',
|
|
248
|
+
'.php': 'PHP',
|
|
249
|
+
'.rb': 'Ruby',
|
|
250
|
+
'.go': 'Go',
|
|
251
|
+
'.rs': 'Rust',
|
|
252
|
+
'.swift': 'Swift',
|
|
253
|
+
'.kt': 'Kotlin',
|
|
254
|
+
'.scala': 'Scala',
|
|
255
|
+
'.vue': 'Vue.js',
|
|
256
|
+
'.svelte': 'Svelte',
|
|
257
|
+
'.dockerfile': 'Dockerfile',
|
|
258
|
+
'.sql': 'SQL',
|
|
259
|
+
'.graphql': 'GraphQL'
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return Object.keys(filesByExtension)
|
|
263
|
+
.sort()
|
|
264
|
+
.map(ext => ({
|
|
265
|
+
extension: ext,
|
|
266
|
+
description: extensionDescriptions[ext] || 'Unknown',
|
|
267
|
+
count: filesByExtension[ext].length,
|
|
268
|
+
files: filesByExtension[ext]
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Calculate total statistics for scanned files
|
|
274
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
275
|
+
* @returns {object} Statistics object
|
|
276
|
+
*/
|
|
277
|
+
calculateStatistics(filesByExtension) {
|
|
278
|
+
let totalFiles = 0;
|
|
279
|
+
let totalSize = 0;
|
|
280
|
+
const extensionCount = Object.keys(filesByExtension).length;
|
|
281
|
+
|
|
282
|
+
Object.values(filesByExtension).forEach(files => {
|
|
283
|
+
totalFiles += files.length;
|
|
284
|
+
totalSize += files.reduce((sum, file) => sum + file.size, 0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
totalFiles,
|
|
289
|
+
totalSize,
|
|
290
|
+
extensionCount,
|
|
291
|
+
averageFileSize: totalFiles > 0 ? Math.round(totalSize / totalFiles) : 0,
|
|
292
|
+
totalSizeFormatted: this.formatFileSize(totalSize)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Format file size in human readable format
|
|
298
|
+
* @param {number} bytes - Size in bytes
|
|
299
|
+
* @returns {string} Formatted size string
|
|
300
|
+
*/
|
|
301
|
+
formatFileSize(bytes) {
|
|
302
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
303
|
+
let size = bytes;
|
|
304
|
+
let unitIndex = 0;
|
|
305
|
+
|
|
306
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
307
|
+
size /= 1024;
|
|
308
|
+
unitIndex++;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Display scan results summary
|
|
316
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
317
|
+
*/
|
|
318
|
+
displayScanSummary(filesByExtension) {
|
|
319
|
+
const stats = this.calculateStatistics(filesByExtension);
|
|
320
|
+
const extensions = Object.keys(filesByExtension).sort();
|
|
321
|
+
|
|
322
|
+
console.log(chalk.cyan('\nScan Summary:'));
|
|
323
|
+
console.log(chalk.gray(` Extensions found: ${extensions.join(', ')}`));
|
|
324
|
+
console.log(chalk.gray(` Total files: ${stats.totalFiles}`));
|
|
325
|
+
console.log(chalk.gray(` Total size: ${stats.totalSizeFormatted}`));
|
|
326
|
+
console.log();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export default Scanner;
|