codesummary 1.0.2 → 1.1.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.
@@ -1,427 +1,827 @@
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;
1
+ import chalk from "chalk";
2
+ import fs from "fs-extra";
3
+ import inquirer from "inquirer";
4
+ import os from "os";
5
+ import path from "path";
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
+ configVersion: "1.1.0", // Version tracking for migrations
44
+ output: {
45
+ mode: "fixed",
46
+ fixedPath: path.join(os.homedir(), "Desktop", "CodeSummaries"),
47
+ },
48
+ allowedExtensions: [
49
+ ".json",
50
+ ".ts",
51
+ ".js",
52
+ ".jsx",
53
+ ".tsx",
54
+ ".xml",
55
+ ".html",
56
+ ".css",
57
+ ".scss",
58
+ ".md",
59
+ ".txt",
60
+ ".py",
61
+ ".java",
62
+ ".cs",
63
+ ".cpp",
64
+ ".c",
65
+ ".h",
66
+ ".yaml",
67
+ ".yml",
68
+ ".sh",
69
+ ".bat",
70
+ ],
71
+ excludeDirs: [
72
+ "node_modules",
73
+ ".git",
74
+ ".vscode",
75
+ "dist",
76
+ "build",
77
+ "coverage",
78
+ "out",
79
+ "__pycache__",
80
+ ".next",
81
+ ".nuxt",
82
+ ],
83
+ excludeFiles: [
84
+ "*-lock.json", // package-lock.json, yarn.lock, etc.
85
+ "*.lock", // Cargo.lock, Gemfile.lock, etc.
86
+ "composer.lock", // PHP Composer lock
87
+ "Pipfile.lock", // Python Pipfile lock
88
+ "*.min.js", // Minified JavaScript
89
+ "*.min.css", // Minified CSS
90
+ "*.map", // Source maps
91
+ ".DS_Store", // macOS metadata
92
+ "Thumbs.db", // Windows thumbnail cache
93
+ "*-lock.yaml", // YAML lock files
94
+ ],
95
+ styles: {
96
+ colors: {
97
+ title: "#333353",
98
+ section: "#00FFB9",
99
+ text: "#333333",
100
+ error: "#FF4D4D",
101
+ footer: "#666666",
102
+ },
103
+ layout: {
104
+ marginLeft: 40,
105
+ marginTop: 40,
106
+ marginRight: 40,
107
+ footerHeight: 20,
108
+ },
109
+ fonts: {
110
+ base: "Source Code Pro",
111
+ },
112
+ },
113
+ settings: {
114
+ documentTitle: "Project Code Summary",
115
+ maxFilesBeforePrompt: 500,
116
+ },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Check if configuration file exists
122
+ * @returns {boolean} True if config exists
123
+ */
124
+ configExists() {
125
+ return fs.existsSync(this.configPath);
126
+ }
127
+
128
+ /**
129
+ * Load configuration from file or return null if not found/corrupted
130
+ * @returns {object|null} Configuration object or null
131
+ */
132
+ async loadConfig() {
133
+ try {
134
+ if (!this.configExists()) {
135
+ return null;
136
+ }
137
+
138
+ const configData = await fs.readJSON(this.configPath);
139
+
140
+ // Migrate configuration if needed (add missing fields)
141
+ const migratedConfig = this.migrateConfig(configData);
142
+
143
+ // Validate configuration structure
144
+ ErrorHandler.validateConfig(migratedConfig);
145
+
146
+ // Save migrated config back if it was changed
147
+ if (JSON.stringify(configData) !== JSON.stringify(migratedConfig)) {
148
+ await this.saveConfig(migratedConfig);
149
+
150
+ // Show user-friendly notification about what was updated
151
+ if (this._pendingNotification) {
152
+ await this.notifyConfigUpdates(
153
+ this._pendingNotification.newExclusions,
154
+ this._pendingNotification.newDirs,
155
+ this._pendingNotification.newExtensions
156
+ );
157
+ this._pendingNotification = null;
158
+ }
159
+ }
160
+
161
+ return migratedConfig;
162
+ } catch (error) {
163
+ if (error.code === "ENOENT") {
164
+ return null;
165
+ }
166
+
167
+ if (error.message.includes("JSON")) {
168
+ console.log(
169
+ chalk.red("ERROR: Configuration file contains invalid JSON.")
170
+ );
171
+ } else if (error.message.includes("Configuration")) {
172
+ console.log(
173
+ chalk.red("ERROR: Configuration file structure is invalid.")
174
+ );
175
+ console.log(chalk.gray("Details:"), error.message);
176
+ } else {
177
+ ErrorHandler.handleFileSystemError(
178
+ error,
179
+ "read configuration",
180
+ this.configPath
181
+ );
182
+ return null;
183
+ }
184
+
185
+ const { shouldReset } = await inquirer.prompt([
186
+ {
187
+ type: "confirm",
188
+ name: "shouldReset",
189
+ message: "Do you want to reset the configuration?",
190
+ default: true,
191
+ },
192
+ ]);
193
+
194
+ if (shouldReset) {
195
+ await this.resetConfig();
196
+ return await this.runFirstTimeSetup();
197
+ } else {
198
+ await ErrorHandler.safeExit(1, "Configuration setup cancelled");
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Save configuration to file
205
+ * @param {object} config Configuration object to save
206
+ */
207
+ async saveConfig(config) {
208
+ try {
209
+ // Validate configuration before saving
210
+ ErrorHandler.validateConfig(config);
211
+
212
+ // Ensure config directory exists
213
+ await fs.ensureDir(this.configDir);
214
+
215
+ // Save configuration with pretty formatting
216
+ await fs.writeJSON(this.configPath, config, { spaces: 2 });
217
+
218
+ console.log(chalk.green(`Configuration saved to ${this.configPath}`));
219
+ } catch (error) {
220
+ if (error.message.includes("Configuration")) {
221
+ console.error(
222
+ chalk.red("ERROR: Invalid configuration:"),
223
+ error.message
224
+ );
225
+ } else {
226
+ ErrorHandler.handleFileSystemError(
227
+ error,
228
+ "save configuration",
229
+ this.configPath
230
+ );
231
+ }
232
+ await ErrorHandler.safeExit(1, "Configuration save failed");
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Smart merge arrays preserving user customizations while adding new defaults
238
+ * @param {Array} userArray - User's current array
239
+ * @param {Array} defaultArray - New default array
240
+ * @returns {object} Object with merged array and new items
241
+ */
242
+ smartMergeArrays(userArray, defaultArray) {
243
+ if (!Array.isArray(userArray)) userArray = [];
244
+ if (!Array.isArray(defaultArray)) defaultArray = [];
245
+
246
+ // Find new items that user doesn't have
247
+ const newItems = defaultArray.filter(item => !userArray.includes(item));
248
+
249
+ // Combine arrays removing duplicates, preserving user's order first
250
+ const merged = [...userArray, ...newItems];
251
+
252
+ return { merged, newItems };
253
+ }
254
+
255
+ /**
256
+ * Show user-friendly notification about configuration updates
257
+ * @param {Array} newExclusions - New exclusion patterns added
258
+ * @param {Array} newDirs - New directories added
259
+ * @param {Array} newExtensions - New extensions added
260
+ */
261
+ async notifyConfigUpdates(newExclusions = [], newDirs = [], newExtensions = []) {
262
+ if (newExclusions.length === 0 && newDirs.length === 0 && newExtensions.length === 0) {
263
+ return;
264
+ }
265
+
266
+ console.log(chalk.cyan("\nšŸ“‹ Configuration Updated"));
267
+ console.log(chalk.gray("New items have been added to improve file scanning:\n"));
268
+
269
+ if (newExclusions.length > 0) {
270
+ console.log(chalk.green("āœ“ New file patterns to skip:"));
271
+ newExclusions.forEach(pattern => {
272
+ console.log(chalk.gray(` • ${pattern} (saves processing time)`));
273
+ });
274
+ console.log();
275
+ }
276
+
277
+ if (newDirs.length > 0) {
278
+ console.log(chalk.green("āœ“ New directories to skip:"));
279
+ newDirs.forEach(dir => {
280
+ console.log(chalk.gray(` • ${dir}/ (improves performance)`));
281
+ });
282
+ console.log();
283
+ }
284
+
285
+ if (newExtensions.length > 0) {
286
+ console.log(chalk.green("āœ“ New file types to include:"));
287
+ newExtensions.forEach(ext => {
288
+ console.log(chalk.gray(` • *${ext} files`));
289
+ });
290
+ console.log();
291
+ }
292
+
293
+ console.log(chalk.gray("Your custom settings have been preserved! ✨\n"));
294
+ }
295
+
296
+ /**
297
+ * Migrate old configuration to new format with intelligent merging
298
+ * @param {object} oldConfig - Old configuration object
299
+ * @returns {object} Migrated configuration
300
+ */
301
+ migrateConfig(oldConfig) {
302
+ const defaultConfig = this.getDefaultConfig();
303
+ const migratedConfig = { ...oldConfig };
304
+
305
+ // Track what's new for user notification
306
+ let newExclusions = [];
307
+ let newDirs = [];
308
+ let newExtensions = [];
309
+
310
+ // Add config version if missing
311
+ if (!migratedConfig.configVersion) {
312
+ migratedConfig.configVersion = defaultConfig.configVersion;
313
+ }
314
+
315
+ // Smart merge for excludeFiles
316
+ if (migratedConfig.excludeFiles) {
317
+ const { merged, newItems } = this.smartMergeArrays(
318
+ migratedConfig.excludeFiles,
319
+ defaultConfig.excludeFiles
320
+ );
321
+ migratedConfig.excludeFiles = merged;
322
+ newExclusions = newItems;
323
+ } else {
324
+ migratedConfig.excludeFiles = defaultConfig.excludeFiles;
325
+ newExclusions = defaultConfig.excludeFiles;
326
+ }
327
+
328
+ // Smart merge for excludeDirs
329
+ if (migratedConfig.excludeDirs) {
330
+ const { merged, newItems } = this.smartMergeArrays(
331
+ migratedConfig.excludeDirs,
332
+ defaultConfig.excludeDirs
333
+ );
334
+ migratedConfig.excludeDirs = merged;
335
+ newDirs = newItems;
336
+ } else {
337
+ migratedConfig.excludeDirs = defaultConfig.excludeDirs;
338
+ newDirs = defaultConfig.excludeDirs;
339
+ }
340
+
341
+ // Smart merge for allowedExtensions
342
+ if (migratedConfig.allowedExtensions) {
343
+ const { merged, newItems } = this.smartMergeArrays(
344
+ migratedConfig.allowedExtensions,
345
+ defaultConfig.allowedExtensions
346
+ );
347
+ migratedConfig.allowedExtensions = merged;
348
+ newExtensions = newItems;
349
+ } else {
350
+ migratedConfig.allowedExtensions = defaultConfig.allowedExtensions;
351
+ newExtensions = defaultConfig.allowedExtensions;
352
+ }
353
+
354
+ // Add any other missing scalar fields from defaults
355
+ Object.keys(defaultConfig).forEach((key) => {
356
+ if (!migratedConfig.hasOwnProperty(key) && !Array.isArray(defaultConfig[key])) {
357
+ migratedConfig[key] = defaultConfig[key];
358
+ }
359
+ });
360
+
361
+ // Show user-friendly notification about updates (will be called by loadConfig)
362
+ this._pendingNotification = { newExclusions, newDirs, newExtensions };
363
+
364
+ return migratedConfig;
365
+ }
366
+
367
+ /**
368
+ * Delete existing configuration file
369
+ */
370
+ async resetConfig() {
371
+ try {
372
+ if (this.configExists()) {
373
+ await fs.remove(this.configPath);
374
+ console.log(chalk.yellow("Configuration reset successfully."));
375
+ }
376
+ } catch (error) {
377
+ console.error(
378
+ chalk.red("ERROR: Failed to reset configuration:"),
379
+ error.message
380
+ );
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Run the first-time setup wizard
386
+ * @returns {object} New configuration object
387
+ */
388
+ async runFirstTimeSetup() {
389
+ console.log(chalk.cyan("Welcome to CodeSummary!"));
390
+ console.log(chalk.gray("No configuration found. Starting setup...\n"));
391
+
392
+ const answers = await inquirer.prompt([
393
+ {
394
+ type: "list",
395
+ name: "outputMode",
396
+ message: "Where should the PDF be generated by default?",
397
+ choices: [
398
+ {
399
+ name: "Current working directory (relative mode)",
400
+ value: "relative",
401
+ },
402
+ {
403
+ name: "Fixed folder (absolute mode)",
404
+ value: "fixed",
405
+ },
406
+ ],
407
+ default: "fixed",
408
+ },
409
+ {
410
+ type: "input",
411
+ name: "fixedPath",
412
+ message: "Enter absolute path for fixed folder:",
413
+ default: path.join(os.homedir(), "Desktop", "CodeSummaries"),
414
+ when: (answers) => answers.outputMode === "fixed",
415
+ validate: (input) => {
416
+ if (!path.isAbsolute(input)) {
417
+ return "Please enter an absolute path";
418
+ }
419
+ return true;
420
+ },
421
+ },
422
+ ]);
423
+
424
+ // Create the config object
425
+ const config = { ...this.defaultConfig };
426
+ config.output.mode = answers.outputMode;
427
+
428
+ if (answers.outputMode === "fixed") {
429
+ config.output.fixedPath = path.resolve(answers.fixedPath);
430
+
431
+ // Atomically ensure directory exists
432
+ try {
433
+ // Use ensureDir which is atomic and handles race conditions
434
+ await fs.ensureDir(config.output.fixedPath);
435
+
436
+ // Verify the directory was created and is accessible
437
+ const stats = await fs.stat(config.output.fixedPath);
438
+ if (!stats.isDirectory()) {
439
+ throw new Error("Path exists but is not a directory");
440
+ }
441
+
442
+ // Test write permissions
443
+ const testFile = path.join(config.output.fixedPath, ".write-test");
444
+ try {
445
+ await fs.writeFile(testFile, "test");
446
+ await fs.remove(testFile);
447
+ } catch (writeError) {
448
+ throw new Error(`Directory not writable: ${writeError.message}`);
449
+ }
450
+
451
+ console.log(
452
+ chalk.green(
453
+ `SUCCESS: Output directory ready: ${config.output.fixedPath}`
454
+ )
455
+ );
456
+ } catch (error) {
457
+ // If directory creation fails, ask user for confirmation
458
+ console.log(
459
+ chalk.yellow(
460
+ `WARNING: Could not prepare output directory: ${error.message}`
461
+ )
462
+ );
463
+
464
+ const { createDir } = await inquirer.prompt([
465
+ {
466
+ type: "confirm",
467
+ name: "createDir",
468
+ message: `Try to create directory ${config.output.fixedPath} anyway?`,
469
+ default: true,
470
+ },
471
+ ]);
472
+
473
+ if (createDir) {
474
+ try {
475
+ // Force creation with more permissive mode
476
+ await fs.ensureDir(config.output.fixedPath, { mode: 0o755 });
477
+ console.log(
478
+ chalk.green(
479
+ `SUCCESS: Created directory: ${config.output.fixedPath}`
480
+ )
481
+ );
482
+ } catch (retryError) {
483
+ console.error(
484
+ chalk.red("ERROR: Failed to create directory:"),
485
+ retryError.message
486
+ );
487
+ await ErrorHandler.safeExit(1, "Directory creation failed");
488
+ }
489
+ } else {
490
+ console.log(
491
+ chalk.yellow(
492
+ "WARNING: Continuing without creating directory. PDF generation may fail."
493
+ )
494
+ );
495
+ }
496
+ }
497
+ }
498
+
499
+ // Save the configuration
500
+ await this.saveConfig(config);
501
+
502
+ console.log(chalk.green("\nSUCCESS: Setup completed successfully!\n"));
503
+
504
+ return config;
505
+ }
506
+
507
+ /**
508
+ * Launch interactive configuration editor
509
+ * @param {object} currentConfig Current configuration to edit
510
+ * @returns {object} Updated configuration
511
+ */
512
+ async editConfig(currentConfig) {
513
+ console.log(chalk.cyan("Configuration Editor\n"));
514
+
515
+ const choices = [
516
+ "Output Settings",
517
+ "File Extensions",
518
+ "Excluded Directories",
519
+ "Excluded Files",
520
+ "PDF Styling",
521
+ "General Settings",
522
+ "Save and Exit",
523
+ ];
524
+
525
+ let config = { ...currentConfig };
526
+ let editing = true;
527
+
528
+ while (editing) {
529
+ const { section } = await inquirer.prompt([
530
+ {
531
+ type: "list",
532
+ name: "section",
533
+ message: "Select section to edit:",
534
+ choices,
535
+ },
536
+ ]);
537
+
538
+ switch (section) {
539
+ case "Output Settings":
540
+ config = await this.editOutputSettings(config);
541
+ break;
542
+ case "File Extensions":
543
+ config = await this.editAllowedExtensions(config);
544
+ break;
545
+ case "Excluded Directories":
546
+ config = await this.editExcludedDirs(config);
547
+ break;
548
+ case "Excluded Files":
549
+ config = await this.editExcludedFiles(config);
550
+ break;
551
+ case "PDF Styling":
552
+ console.log(
553
+ chalk.yellow("PDF styling editor coming in future version")
554
+ );
555
+ break;
556
+ case "General Settings":
557
+ config = await this.editGeneralSettings(config);
558
+ break;
559
+ case "Save and Exit":
560
+ editing = false;
561
+ break;
562
+ }
563
+ }
564
+
565
+ await this.saveConfig(config);
566
+ return config;
567
+ }
568
+
569
+ /**
570
+ * Edit output settings
571
+ * @param {object} config Current configuration
572
+ * @returns {object} Updated configuration
573
+ */
574
+ async editOutputSettings(config) {
575
+ const answers = await inquirer.prompt([
576
+ {
577
+ type: "list",
578
+ name: "mode",
579
+ message: "Output mode:",
580
+ choices: [
581
+ { name: "Relative (current directory)", value: "relative" },
582
+ { name: "Fixed path", value: "fixed" },
583
+ ],
584
+ default: config.output.mode,
585
+ },
586
+ {
587
+ type: "input",
588
+ name: "fixedPath",
589
+ message: "Fixed path:",
590
+ default: config.output.fixedPath,
591
+ when: (answers) => answers.mode === "fixed",
592
+ },
593
+ ]);
594
+
595
+ config.output.mode = answers.mode;
596
+ if (answers.fixedPath) {
597
+ config.output.fixedPath = path.resolve(answers.fixedPath);
598
+ }
599
+
600
+ return config;
601
+ }
602
+
603
+ /**
604
+ * Edit allowed extensions
605
+ * @param {object} config Current configuration
606
+ * @returns {object} Updated configuration
607
+ */
608
+ async editAllowedExtensions(config) {
609
+ const { extensions } = await inquirer.prompt([
610
+ {
611
+ type: "input",
612
+ name: "extensions",
613
+ message: "Allowed extensions (comma-separated):",
614
+ default: config.allowedExtensions.join(", "),
615
+ validate: (input) => {
616
+ if (!input.trim()) {
617
+ return "Please enter at least one extension";
618
+ }
619
+ return true;
620
+ },
621
+ },
622
+ ]);
623
+
624
+ config.allowedExtensions = extensions
625
+ .split(",")
626
+ .map((ext) => ext.trim())
627
+ .filter((ext) => ext.length > 0)
628
+ .map((ext) => (ext.startsWith(".") ? ext : "." + ext));
629
+
630
+ return config;
631
+ }
632
+
633
+ /**
634
+ * Edit excluded directories
635
+ * @param {object} config Current configuration
636
+ * @returns {object} Updated configuration
637
+ */
638
+ async editExcludedDirs(config) {
639
+ const { dirs } = await inquirer.prompt([
640
+ {
641
+ type: "input",
642
+ name: "dirs",
643
+ message: "Excluded directories (comma-separated):",
644
+ default: config.excludeDirs.join(", "),
645
+ },
646
+ ]);
647
+
648
+ config.excludeDirs = dirs
649
+ .split(",")
650
+ .map((dir) => dir.trim())
651
+ .filter((dir) => dir.length > 0);
652
+
653
+ return config;
654
+ }
655
+
656
+ /**
657
+ * Edit excluded files patterns
658
+ * @param {object} config Current configuration
659
+ * @returns {object} Updated configuration
660
+ */
661
+ async editExcludedFiles(config) {
662
+ // Ensure excludeFiles exists for backward compatibility
663
+ if (!config.excludeFiles) {
664
+ config.excludeFiles = [
665
+ "*-lock.json",
666
+ "*.lock",
667
+ "composer.lock",
668
+ "Pipfile.lock",
669
+ "*.min.js",
670
+ "*.min.css",
671
+ "*.map",
672
+ ".DS_Store",
673
+ "Thumbs.db",
674
+ ];
675
+ }
676
+
677
+ const { files } = await inquirer.prompt([
678
+ {
679
+ type: "input",
680
+ name: "files",
681
+ message:
682
+ "Excluded file patterns (comma-separated, supports * wildcards):",
683
+ default: config.excludeFiles.join(", "),
684
+ validate: (input) => {
685
+ if (!input.trim()) {
686
+ return "Enter file patterns or leave empty to clear all exclusions";
687
+ }
688
+ return true;
689
+ },
690
+ },
691
+ ]);
692
+
693
+ if (files.trim()) {
694
+ config.excludeFiles = files
695
+ .split(",")
696
+ .map((pattern) => pattern.trim())
697
+ .filter((pattern) => pattern.length > 0);
698
+ } else {
699
+ config.excludeFiles = [];
700
+ }
701
+
702
+ console.log(
703
+ chalk.green(
704
+ `\nāœ“ Updated excluded files: ${config.excludeFiles.length} patterns`
705
+ )
706
+ );
707
+ if (config.excludeFiles.length > 0) {
708
+ console.log(chalk.gray(" Examples of files that will be excluded:"));
709
+ config.excludeFiles.slice(0, 3).forEach((pattern) => {
710
+ console.log(chalk.gray(` - ${pattern}`));
711
+ });
712
+ if (config.excludeFiles.length > 3) {
713
+ console.log(
714
+ chalk.gray(` ... and ${config.excludeFiles.length - 3} more`)
715
+ );
716
+ }
717
+ }
718
+
719
+ return config;
720
+ }
721
+
722
+ /**
723
+ * Edit general settings
724
+ * @param {object} config Current configuration
725
+ * @returns {object} Updated configuration
726
+ */
727
+ async editGeneralSettings(config) {
728
+ const answers = await inquirer.prompt([
729
+ {
730
+ type: "input",
731
+ name: "documentTitle",
732
+ message: "Document title:",
733
+ default: config.settings.documentTitle,
734
+ },
735
+ {
736
+ type: "number",
737
+ name: "maxFilesBeforePrompt",
738
+ message: "Max files before warning prompt:",
739
+ default: config.settings.maxFilesBeforePrompt,
740
+ validate: (input) => input > 0 || "Must be a positive number",
741
+ },
742
+ ]);
743
+
744
+ config.settings.documentTitle = answers.documentTitle;
745
+ config.settings.maxFilesBeforePrompt = answers.maxFilesBeforePrompt;
746
+
747
+ return config;
748
+ }
749
+
750
+ /**
751
+ * Display current configuration in user-friendly format
752
+ * @param {object} config Configuration to display
753
+ */
754
+ displayConfig(config) {
755
+ console.log(chalk.cyan("\nšŸ“‹ Current Configuration\n"));
756
+
757
+ // Output Settings
758
+ console.log(chalk.green("šŸ“ Output Settings:"));
759
+ if (config.output.mode === "fixed") {
760
+ console.log(chalk.gray(` • Mode: Fixed folder`));
761
+ console.log(chalk.gray(` • Location: ${config.output.fixedPath}`));
762
+ } else {
763
+ console.log(chalk.gray(` • Mode: Current directory`));
764
+ }
765
+ console.log();
766
+
767
+ // File Types
768
+ console.log(chalk.green("šŸ“„ Included File Types:"));
769
+ const extensions = config.allowedExtensions || [];
770
+ if (extensions.length > 0) {
771
+ // Group by category for better readability
772
+ const webFiles = extensions.filter(ext => ['.html', '.css', '.scss', '.js', '.jsx', '.ts', '.tsx'].includes(ext));
773
+ const configFiles = extensions.filter(ext => ['.json', '.yaml', '.yml', '.xml'].includes(ext));
774
+ const docFiles = extensions.filter(ext => ['.md', '.txt'].includes(ext));
775
+ const codeFiles = extensions.filter(ext => ![...webFiles, ...configFiles, ...docFiles].includes(ext));
776
+
777
+ if (webFiles.length > 0) {
778
+ console.log(chalk.gray(` • Web files: ${webFiles.join(', ')}`));
779
+ }
780
+ if (codeFiles.length > 0) {
781
+ console.log(chalk.gray(` • Code files: ${codeFiles.join(', ')}`));
782
+ }
783
+ if (configFiles.length > 0) {
784
+ console.log(chalk.gray(` • Config files: ${configFiles.join(', ')}`));
785
+ }
786
+ if (docFiles.length > 0) {
787
+ console.log(chalk.gray(` • Documentation: ${docFiles.join(', ')}`));
788
+ }
789
+ } else {
790
+ console.log(chalk.gray(` • No file types configured`));
791
+ }
792
+ console.log();
793
+
794
+ // Excluded Directories
795
+ console.log(chalk.green("🚫 Skipped Directories:"));
796
+ const excludeDirs = config.excludeDirs || [];
797
+ if (excludeDirs.length > 0) {
798
+ excludeDirs.forEach(dir => {
799
+ console.log(chalk.gray(` • ${dir}/`));
800
+ });
801
+ } else {
802
+ console.log(chalk.gray(` • None`));
803
+ }
804
+ console.log();
805
+
806
+ // Excluded Files
807
+ console.log(chalk.green("🚫 Skipped File Patterns:"));
808
+ const excludeFiles = config.excludeFiles || [];
809
+ if (excludeFiles.length > 0) {
810
+ excludeFiles.forEach(pattern => {
811
+ console.log(chalk.gray(` • ${pattern}`));
812
+ });
813
+ } else {
814
+ console.log(chalk.gray(` • None`));
815
+ }
816
+ console.log();
817
+
818
+ // General Settings
819
+ console.log(chalk.green("āš™ļø General Settings:"));
820
+ console.log(chalk.gray(` • Document title: "${config.settings?.documentTitle || 'Project Code Summary'}"`));
821
+ console.log(chalk.gray(` • File warning threshold: ${config.settings?.maxFilesBeforePrompt || 500} files`));
822
+ console.log(chalk.gray(` • Configuration version: ${config.configVersion || 'legacy'}`));
823
+ console.log();
824
+ }
825
+ }
826
+
827
+ export default ConfigManager;