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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ErrorHandler from './errorHandler.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration Manager for CodeSummary
|
|
10
|
+
* Handles global configuration storage, first-run setup, and user preferences
|
|
11
|
+
* Cross-platform compatible with POSIX and Windows systems
|
|
12
|
+
*/
|
|
13
|
+
export class ConfigManager {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.configDir = this.getConfigDirectory();
|
|
16
|
+
this.configPath = path.join(this.configDir, 'config.json');
|
|
17
|
+
this.defaultConfig = this.getDefaultConfig();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the appropriate configuration directory based on platform
|
|
22
|
+
* @returns {string} Configuration directory path
|
|
23
|
+
*/
|
|
24
|
+
getConfigDirectory() {
|
|
25
|
+
const platform = os.platform();
|
|
26
|
+
const homeDir = os.homedir();
|
|
27
|
+
|
|
28
|
+
if (platform === 'win32') {
|
|
29
|
+
// Windows: %APPDATA%\CodeSummary\
|
|
30
|
+
return path.join(process.env.APPDATA || homeDir, 'CodeSummary');
|
|
31
|
+
} else {
|
|
32
|
+
// POSIX (Linux/macOS): ~/.codesummary/
|
|
33
|
+
return path.join(homeDir, '.codesummary');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get default configuration object
|
|
39
|
+
* @returns {object} Default configuration
|
|
40
|
+
*/
|
|
41
|
+
getDefaultConfig() {
|
|
42
|
+
return {
|
|
43
|
+
output: {
|
|
44
|
+
mode: 'fixed',
|
|
45
|
+
fixedPath: path.join(os.homedir(), 'Desktop', 'CodeSummaries')
|
|
46
|
+
},
|
|
47
|
+
allowedExtensions: [
|
|
48
|
+
'.json', '.ts', '.js', '.jsx', '.tsx', '.xml', '.html',
|
|
49
|
+
'.css', '.scss', '.md', '.txt', '.py', '.java', '.cs',
|
|
50
|
+
'.cpp', '.c', '.h', '.yaml', '.yml', '.sh', '.bat'
|
|
51
|
+
],
|
|
52
|
+
excludeDirs: [
|
|
53
|
+
'node_modules', '.git', '.vscode', 'dist', 'build',
|
|
54
|
+
'coverage', 'out', '__pycache__', '.next', '.nuxt'
|
|
55
|
+
],
|
|
56
|
+
styles: {
|
|
57
|
+
colors: {
|
|
58
|
+
title: '#333353',
|
|
59
|
+
section: '#00FFB9',
|
|
60
|
+
text: '#333333',
|
|
61
|
+
error: '#FF4D4D',
|
|
62
|
+
footer: '#666666'
|
|
63
|
+
},
|
|
64
|
+
layout: {
|
|
65
|
+
marginLeft: 40,
|
|
66
|
+
marginTop: 40,
|
|
67
|
+
marginRight: 40,
|
|
68
|
+
footerHeight: 20
|
|
69
|
+
},
|
|
70
|
+
fonts: {
|
|
71
|
+
base: 'Source Code Pro'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
settings: {
|
|
75
|
+
documentTitle: 'Project Code Summary',
|
|
76
|
+
maxFilesBeforePrompt: 500
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if configuration file exists
|
|
83
|
+
* @returns {boolean} True if config exists
|
|
84
|
+
*/
|
|
85
|
+
configExists() {
|
|
86
|
+
return fs.existsSync(this.configPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load configuration from file or return null if not found/corrupted
|
|
91
|
+
* @returns {object|null} Configuration object or null
|
|
92
|
+
*/
|
|
93
|
+
async loadConfig() {
|
|
94
|
+
try {
|
|
95
|
+
if (!this.configExists()) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const configData = await fs.readJSON(this.configPath);
|
|
100
|
+
|
|
101
|
+
// Validate configuration structure
|
|
102
|
+
ErrorHandler.validateConfig(configData);
|
|
103
|
+
|
|
104
|
+
return configData;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.code === 'ENOENT') {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (error.message.includes('JSON')) {
|
|
111
|
+
console.log(chalk.red('ERROR: Configuration file contains invalid JSON.'));
|
|
112
|
+
} else if (error.message.includes('Configuration')) {
|
|
113
|
+
console.log(chalk.red('ERROR: Configuration file structure is invalid.'));
|
|
114
|
+
console.log(chalk.gray('Details:'), error.message);
|
|
115
|
+
} else {
|
|
116
|
+
ErrorHandler.handleFileSystemError(error, 'read configuration', this.configPath);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { shouldReset } = await inquirer.prompt([{
|
|
121
|
+
type: 'confirm',
|
|
122
|
+
name: 'shouldReset',
|
|
123
|
+
message: 'Do you want to reset the configuration?',
|
|
124
|
+
default: true
|
|
125
|
+
}]);
|
|
126
|
+
|
|
127
|
+
if (shouldReset) {
|
|
128
|
+
await this.resetConfig();
|
|
129
|
+
return await this.runFirstTimeSetup();
|
|
130
|
+
} else {
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Save configuration to file
|
|
138
|
+
* @param {object} config Configuration object to save
|
|
139
|
+
*/
|
|
140
|
+
async saveConfig(config) {
|
|
141
|
+
try {
|
|
142
|
+
// Validate configuration before saving
|
|
143
|
+
ErrorHandler.validateConfig(config);
|
|
144
|
+
|
|
145
|
+
// Ensure config directory exists
|
|
146
|
+
await fs.ensureDir(this.configDir);
|
|
147
|
+
|
|
148
|
+
// Save configuration with pretty formatting
|
|
149
|
+
await fs.writeJSON(this.configPath, config, { spaces: 2 });
|
|
150
|
+
|
|
151
|
+
console.log(chalk.green(`Configuration saved to ${this.configPath}`));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.message.includes('Configuration')) {
|
|
154
|
+
console.error(chalk.red('ERROR: Invalid configuration:'), error.message);
|
|
155
|
+
} else {
|
|
156
|
+
ErrorHandler.handleFileSystemError(error, 'save configuration', this.configPath);
|
|
157
|
+
}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete existing configuration file
|
|
164
|
+
*/
|
|
165
|
+
async resetConfig() {
|
|
166
|
+
try {
|
|
167
|
+
if (this.configExists()) {
|
|
168
|
+
await fs.remove(this.configPath);
|
|
169
|
+
console.log(chalk.yellow('Configuration reset successfully.'));
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(chalk.red('ERROR: Failed to reset configuration:'), error.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run the first-time setup wizard
|
|
178
|
+
* @returns {object} New configuration object
|
|
179
|
+
*/
|
|
180
|
+
async runFirstTimeSetup() {
|
|
181
|
+
console.log(chalk.cyan('Welcome to CodeSummary!'));
|
|
182
|
+
console.log(chalk.gray('No configuration found. Starting setup...\n'));
|
|
183
|
+
|
|
184
|
+
const answers = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: 'list',
|
|
187
|
+
name: 'outputMode',
|
|
188
|
+
message: 'Where should the PDF be generated by default?',
|
|
189
|
+
choices: [
|
|
190
|
+
{
|
|
191
|
+
name: 'Current working directory (relative mode)',
|
|
192
|
+
value: 'relative'
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'Fixed folder (absolute mode)',
|
|
196
|
+
value: 'fixed'
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
default: 'fixed'
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'input',
|
|
203
|
+
name: 'fixedPath',
|
|
204
|
+
message: 'Enter absolute path for fixed folder:',
|
|
205
|
+
default: path.join(os.homedir(), 'Desktop', 'CodeSummaries'),
|
|
206
|
+
when: (answers) => answers.outputMode === 'fixed',
|
|
207
|
+
validate: (input) => {
|
|
208
|
+
if (!path.isAbsolute(input)) {
|
|
209
|
+
return 'Please enter an absolute path';
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
// Create the config object
|
|
217
|
+
const config = { ...this.defaultConfig };
|
|
218
|
+
config.output.mode = answers.outputMode;
|
|
219
|
+
|
|
220
|
+
if (answers.outputMode === 'fixed') {
|
|
221
|
+
config.output.fixedPath = path.resolve(answers.fixedPath);
|
|
222
|
+
|
|
223
|
+
// Offer to create the directory if it doesn't exist
|
|
224
|
+
if (!fs.existsSync(config.output.fixedPath)) {
|
|
225
|
+
const { createDir } = await inquirer.prompt([{
|
|
226
|
+
type: 'confirm',
|
|
227
|
+
name: 'createDir',
|
|
228
|
+
message: `Directory ${config.output.fixedPath} does not exist. Create it?`,
|
|
229
|
+
default: true
|
|
230
|
+
}]);
|
|
231
|
+
|
|
232
|
+
if (createDir) {
|
|
233
|
+
try {
|
|
234
|
+
await fs.ensureDir(config.output.fixedPath);
|
|
235
|
+
console.log(chalk.green(`SUCCESS: Created directory: ${config.output.fixedPath}`));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(chalk.red('ERROR: Failed to create directory:'), error.message);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Save the configuration
|
|
245
|
+
await this.saveConfig(config);
|
|
246
|
+
|
|
247
|
+
console.log(chalk.green('\nSUCCESS: Setup completed successfully!\n'));
|
|
248
|
+
|
|
249
|
+
return config;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Launch interactive configuration editor
|
|
254
|
+
* @param {object} currentConfig Current configuration to edit
|
|
255
|
+
* @returns {object} Updated configuration
|
|
256
|
+
*/
|
|
257
|
+
async editConfig(currentConfig) {
|
|
258
|
+
console.log(chalk.cyan('Configuration Editor\n'));
|
|
259
|
+
|
|
260
|
+
const choices = [
|
|
261
|
+
'Output Settings',
|
|
262
|
+
'File Extensions',
|
|
263
|
+
'Excluded Directories',
|
|
264
|
+
'PDF Styling',
|
|
265
|
+
'General Settings',
|
|
266
|
+
'Save and Exit'
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
let config = { ...currentConfig };
|
|
270
|
+
let editing = true;
|
|
271
|
+
|
|
272
|
+
while (editing) {
|
|
273
|
+
const { section } = await inquirer.prompt([{
|
|
274
|
+
type: 'list',
|
|
275
|
+
name: 'section',
|
|
276
|
+
message: 'Select section to edit:',
|
|
277
|
+
choices
|
|
278
|
+
}]);
|
|
279
|
+
|
|
280
|
+
switch (section) {
|
|
281
|
+
case 'Output Settings':
|
|
282
|
+
config = await this.editOutputSettings(config);
|
|
283
|
+
break;
|
|
284
|
+
case 'File Extensions':
|
|
285
|
+
config = await this.editAllowedExtensions(config);
|
|
286
|
+
break;
|
|
287
|
+
case 'Excluded Directories':
|
|
288
|
+
config = await this.editExcludedDirs(config);
|
|
289
|
+
break;
|
|
290
|
+
case 'PDF Styling':
|
|
291
|
+
console.log(chalk.yellow('PDF styling editor coming in future version'));
|
|
292
|
+
break;
|
|
293
|
+
case 'General Settings':
|
|
294
|
+
config = await this.editGeneralSettings(config);
|
|
295
|
+
break;
|
|
296
|
+
case 'Save and Exit':
|
|
297
|
+
editing = false;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await this.saveConfig(config);
|
|
303
|
+
return config;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Edit output settings
|
|
308
|
+
* @param {object} config Current configuration
|
|
309
|
+
* @returns {object} Updated configuration
|
|
310
|
+
*/
|
|
311
|
+
async editOutputSettings(config) {
|
|
312
|
+
const answers = await inquirer.prompt([
|
|
313
|
+
{
|
|
314
|
+
type: 'list',
|
|
315
|
+
name: 'mode',
|
|
316
|
+
message: 'Output mode:',
|
|
317
|
+
choices: [
|
|
318
|
+
{ name: 'Relative (current directory)', value: 'relative' },
|
|
319
|
+
{ name: 'Fixed path', value: 'fixed' }
|
|
320
|
+
],
|
|
321
|
+
default: config.output.mode
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: 'input',
|
|
325
|
+
name: 'fixedPath',
|
|
326
|
+
message: 'Fixed path:',
|
|
327
|
+
default: config.output.fixedPath,
|
|
328
|
+
when: (answers) => answers.mode === 'fixed'
|
|
329
|
+
}
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
config.output.mode = answers.mode;
|
|
333
|
+
if (answers.fixedPath) {
|
|
334
|
+
config.output.fixedPath = path.resolve(answers.fixedPath);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return config;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Edit allowed extensions
|
|
342
|
+
* @param {object} config Current configuration
|
|
343
|
+
* @returns {object} Updated configuration
|
|
344
|
+
*/
|
|
345
|
+
async editAllowedExtensions(config) {
|
|
346
|
+
const { extensions } = await inquirer.prompt([{
|
|
347
|
+
type: 'input',
|
|
348
|
+
name: 'extensions',
|
|
349
|
+
message: 'Allowed extensions (comma-separated):',
|
|
350
|
+
default: config.allowedExtensions.join(', '),
|
|
351
|
+
validate: (input) => {
|
|
352
|
+
if (!input.trim()) {
|
|
353
|
+
return 'Please enter at least one extension';
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}]);
|
|
358
|
+
|
|
359
|
+
config.allowedExtensions = extensions
|
|
360
|
+
.split(',')
|
|
361
|
+
.map(ext => ext.trim())
|
|
362
|
+
.filter(ext => ext.length > 0)
|
|
363
|
+
.map(ext => ext.startsWith('.') ? ext : '.' + ext);
|
|
364
|
+
|
|
365
|
+
return config;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Edit excluded directories
|
|
370
|
+
* @param {object} config Current configuration
|
|
371
|
+
* @returns {object} Updated configuration
|
|
372
|
+
*/
|
|
373
|
+
async editExcludedDirs(config) {
|
|
374
|
+
const { dirs } = await inquirer.prompt([{
|
|
375
|
+
type: 'input',
|
|
376
|
+
name: 'dirs',
|
|
377
|
+
message: 'Excluded directories (comma-separated):',
|
|
378
|
+
default: config.excludeDirs.join(', ')
|
|
379
|
+
}]);
|
|
380
|
+
|
|
381
|
+
config.excludeDirs = dirs
|
|
382
|
+
.split(',')
|
|
383
|
+
.map(dir => dir.trim())
|
|
384
|
+
.filter(dir => dir.length > 0);
|
|
385
|
+
|
|
386
|
+
return config;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Edit general settings
|
|
391
|
+
* @param {object} config Current configuration
|
|
392
|
+
* @returns {object} Updated configuration
|
|
393
|
+
*/
|
|
394
|
+
async editGeneralSettings(config) {
|
|
395
|
+
const answers = await inquirer.prompt([
|
|
396
|
+
{
|
|
397
|
+
type: 'input',
|
|
398
|
+
name: 'documentTitle',
|
|
399
|
+
message: 'Document title:',
|
|
400
|
+
default: config.settings.documentTitle
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
type: 'number',
|
|
404
|
+
name: 'maxFilesBeforePrompt',
|
|
405
|
+
message: 'Max files before warning prompt:',
|
|
406
|
+
default: config.settings.maxFilesBeforePrompt,
|
|
407
|
+
validate: (input) => input > 0 || 'Must be a positive number'
|
|
408
|
+
}
|
|
409
|
+
]);
|
|
410
|
+
|
|
411
|
+
config.settings.documentTitle = answers.documentTitle;
|
|
412
|
+
config.settings.maxFilesBeforePrompt = answers.maxFilesBeforePrompt;
|
|
413
|
+
|
|
414
|
+
return config;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Display current configuration
|
|
419
|
+
* @param {object} config Configuration to display
|
|
420
|
+
*/
|
|
421
|
+
displayConfig(config) {
|
|
422
|
+
console.log(chalk.cyan('\nCurrent Configuration:\n'));
|
|
423
|
+
console.log(JSON.stringify(config, null, 2));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default ConfigManager;
|