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.
@@ -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;