devsplain 1.2.0 → 1.5.1

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/bin/cli.js CHANGED
@@ -1,196 +1,592 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Import required modules.
5
- * @module llm
6
- * @module config
7
- * @module fs
8
- * @module path
9
- * @module readline
10
- */
11
- const { getComments } = require('../lib/llm.js');
12
- const { getConfig } = require('../lib/config.js');
13
- const fs = require('fs');
14
- const path = require('path');
15
- const readline = require('readline');
16
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
17
-
18
- /**
19
- * Asks a question and returns the answer as a promise.
20
- * @param {string} query - The question to be asked.
21
- * @returns {Promise<string>} The answer to the question.
22
- */
23
- const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
24
-
25
- /**
26
- * Get the filepath and arguments from the command line.
27
- * @type {string}
28
- */
29
- const filepath = process.argv[2];
30
- const args = process.argv.slice(3);
31
- let mode = 'default';
32
-
33
- /**
34
- * Determine the mode based on the command line arguments.
35
- * Supported modes are: 'light', 'full', and 'clean'.
36
- */
37
- if (args.includes('--light')) mode = 'light';
38
- if (args.includes('--full')) mode = 'full';
39
- if (args.includes('--clean')) mode = 'clean';
40
- const isDryRun = args.includes('--dry-run');
41
-
42
- /**
43
- * Check if a filepath was provided, if not display usage information.
44
- */
45
- if (!filepath) {
46
- console.log("usage: devsplain <file-or-directory>");
47
- process.exit(1);
48
- }
49
- else if (!fs.existsSync(filepath)) {
50
- console.log(`Error: The path '${filepath}' does not exist.`);
51
- process.exit(1);
52
- }
53
- else {
54
- (async () => {
55
- /**
56
- * Get the configuration.
57
- * @type {object}
58
- */
59
- const config = await getConfig();
60
-
61
- /**
62
- * Process a path, either a file or directory.
63
- * @param {string} targetPath - The path to process.
64
- * @returns {Promise<void>}
65
- */
66
- async function processPath(targetPath) {
67
- /**
68
- * Get the stats of the target path.
69
- * @type {fs.Stats}
70
- */
71
- const stats = fs.statSync(targetPath);
72
-
73
- /**
74
- * If the target path is a directory, process its contents.
75
- */
76
- if (stats.isDirectory()) {
77
- const folderName = path.basename(targetPath);
78
- /**
79
- * List of ignored folders.
80
- * @type {string[]}
81
- */
82
- const ignoredFolders = [
83
- 'node_modules', '.git', 'dist', 'build', 'out',
84
- '.next', '.nuxt', '.svelte-kit',
85
- 'venv', 'env', '.venv',
86
- '.vscode', '.idea', 'coverage'
87
- ];
88
-
89
- /**
90
- * Check if the folder should be ignored.
91
- */
92
- if (ignoredFolders.includes(folderName)) {
93
- // Folder will be skipped
94
- return;
95
- }
96
-
97
- console.log(`\n Scanning directory: ${targetPath}`);
98
- /**
99
- * Read the contents of the directory.
100
- * @type {string[]}
101
- */
102
- const items = fs.readdirSync(targetPath);
103
-
104
- /**
105
- * Process each item in the directory.
106
- */
107
- for (const item of items) {
108
- const fullPath = path.join(targetPath, item);
109
- await processPath(fullPath);
110
- }
111
- }
112
- /**
113
- * If the target path is a file, process it.
114
- */
115
- else if (stats.isFile()) {
116
- /**
117
- * Get the file extension.
118
- * @type {string}
119
- */
120
- const ext = path.extname(targetPath).toLowerCase();
121
- /**
122
- * List of valid file extensions.
123
- * @type {string[]}
124
- */
125
- const validExtensions = [
126
- '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
127
- '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
128
- '.swift', '.kt', '.dart', '.sh'
129
- ];
130
-
131
- /**
132
- * Check if the file extension is valid.
133
- */
134
- if (!validExtensions.includes(ext)) {
135
- // File type is not supported, skipping
136
- return;
137
- }
138
-
139
- const filename = path.basename(targetPath);
140
- /**
141
- * Read the file contents.
142
- * @type {string}
143
- */
144
- const data = fs.readFileSync(targetPath, 'utf-8');
145
-
146
- /**
147
- * Check if the file is empty.
148
- */
149
- if (data.trim() === '') {
150
- console.log(` Skipping ${filename} (Empty File)`);
151
- return;
152
- }
153
-
154
- console.log(` Analyzing ${filename} in ${mode} mode...`);
155
- /**
156
- * Get the commented code.
157
- * @type {string}
158
- */
159
- const commentedCode = await getComments(data, filename, config, mode);
160
-
161
- /**
162
- * If in dry run mode, display a preview of the commented code.
163
- */
164
- if (isDryRun) {
165
- console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
166
- console.log(commentedCode);
167
- console.log(`---------------------------------------\n`);
168
-
169
- /**
170
- * Ask the user if they want to save the commented code.
171
- */
172
- const answer = await askQuestion("Type 'write' to save to file, or press any key to discard ");
173
-
174
- if (answer.toLowerCase() == 'write') {
175
- fs.writeFileSync(targetPath, commentedCode);
176
- console.log(` Successfully saved ${targetPath}`);
177
- } else {
178
- console.log(` Skipped ${targetPath}`);
179
- }
180
- } else {
181
- /**
182
- * Write the commented code to the file.
183
- */
184
- fs.writeFileSync(targetPath, commentedCode);
185
- console.log(` Successfully commented ${targetPath}`);
186
- }
187
- }
188
- }
189
-
190
- /**
191
- * Start processing the provided filepath.
192
- */
193
- await processPath(filepath);
194
- console.log("\n All done!");
195
- })();
1
+
2
+ const { getComments } = require('../lib/llm.js');
3
+ const { getConfig } = require('../lib/config.js');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+ const { execSync } = require('child_process');
8
+
9
+ let rl;
10
+ let askQuestion;
11
+
12
+ /** Checks if the current Git repository has uncommitted changes */
13
+ function isGitDirty() {
14
+ try {
15
+ const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
16
+ if (gitDir === 'true') {
17
+ const status = execSync('git status --porcelain', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
18
+ return status.length > 0;
19
+ }
20
+ } catch (e) {
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /** Determines if a specific line index is inside a string or multiline literal */
26
+ function isLineInsideString(lines, targetLineIndex, ext = '') {
27
+ const isPython = ext.toLowerCase() === '.py';
28
+ let inBacktick = false;
29
+ let inTripleDouble = false;
30
+ let inTripleSingle = false;
31
+ let inSingle = false;
32
+ let inDouble = false;
33
+
34
+ for (let i = 0; i < targetLineIndex; i++) {
35
+ const line = lines[i];
36
+ let j = 0;
37
+ while (j < line.length) {
38
+ if (isPython) {
39
+ if (!inTripleSingle) {
40
+ if (line.slice(j, j + 3) === '"""') {
41
+ inTripleDouble = !inTripleDouble;
42
+ j += 3;
43
+ continue;
44
+ }
45
+ }
46
+ if (!inTripleDouble) {
47
+ if (line.slice(j, j + 3) === "'''") {
48
+ inTripleSingle = !inTripleSingle;
49
+ j += 3;
50
+ continue;
51
+ }
52
+ }
53
+ } else {
54
+ if (!inSingle && !inDouble && line[j] === '`') {
55
+ let escaped = false;
56
+ let k = j - 1;
57
+ while (k >= 0 && line[k] === '\\') {
58
+ escaped = !escaped;
59
+ k--;
60
+ }
61
+ if (!escaped) {
62
+ inBacktick = !inBacktick;
63
+ }
64
+ }
65
+ if (!inBacktick) {
66
+ if (line[j] === '"' && !inSingle) {
67
+ let escaped = false;
68
+ let k = j - 1;
69
+ while (k >= 0 && line[k] === '\\') {
70
+ escaped = !escaped;
71
+ k--;
72
+ }
73
+ if (!escaped) {
74
+ inDouble = !inDouble;
75
+ }
76
+ } else if (line[j] === "'" && !inDouble) {
77
+ let escaped = false;
78
+ let k = j - 1;
79
+ while (k >= 0 && line[k] === '\\') {
80
+ escaped = !escaped;
81
+ k--;
82
+ }
83
+ if (!escaped) {
84
+ inSingle = !inSingle;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ j++;
90
+ }
91
+ if (!isPython) {
92
+ inSingle = false;
93
+ inDouble = false;
94
+ }
95
+ }
96
+ return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
97
+ }
98
+
99
+ /** Analyzes code to identify pure comment lines and block boundaries */
100
+ function analyzeComments(lines, ext = '') {
101
+ const isPython = ext.toLowerCase() === '.py';
102
+ const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
103
+ const analysis = [];
104
+ let inBacktick = false;
105
+ let inTripleDouble = false;
106
+ let inTripleSingle = false;
107
+ let inSingle = false;
108
+ let inDouble = false;
109
+ let inBlockJS = false;
110
+ let inBlockHTML = false;
111
+ for (let i = 0; i < lines.length; i++) {
112
+ const line = lines[i];
113
+ let commentStartIndex = -1;
114
+ let isInsideBlockStart = inBlockJS || inBlockHTML;
115
+ let j = 0;
116
+ while (j < line.length) {
117
+ if (inBlockJS) {
118
+ if (line.slice(j, j + 2) === '*/') {
119
+ inBlockJS = false;
120
+ j += 2;
121
+ continue;
122
+ }
123
+ j++;
124
+ continue;
125
+ }
126
+ if (inBlockHTML) {
127
+ if (line.slice(j, j + 3) === '-->') {
128
+ inBlockHTML = false;
129
+ j += 3;
130
+ continue;
131
+ }
132
+ j++;
133
+ continue;
134
+ }
135
+ if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
136
+ if (isPython) {
137
+ if (line[j] === '#') {
138
+ commentStartIndex = j;
139
+ break;
140
+ }
141
+ } else if (isHTML) {
142
+ if (line.slice(j, j + 4) === '<!--') {
143
+ commentStartIndex = j;
144
+ inBlockHTML = true;
145
+ j += 4;
146
+ continue;
147
+ }
148
+ if (line.slice(j, j + 2) === '//') {
149
+ commentStartIndex = j;
150
+ break;
151
+ }
152
+ } else {
153
+ if (line.slice(j, j + 2) === '//') {
154
+ commentStartIndex = j;
155
+ break;
156
+ }
157
+ if (line.slice(j, j + 2) === '/*') {
158
+ commentStartIndex = j;
159
+ inBlockJS = true;
160
+ j += 2;
161
+ continue;
162
+ }
163
+ if (line[j] === '#') {
164
+ commentStartIndex = j;
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ if (isPython) {
170
+ if (!inTripleSingle) {
171
+ if (line.slice(j, j + 3) === '"""') {
172
+ inTripleDouble = !inTripleDouble;
173
+ j += 3;
174
+ continue;
175
+ }
176
+ }
177
+ if (!inTripleDouble) {
178
+ if (line.slice(j, j + 3) === "'''") {
179
+ inTripleSingle = !inTripleSingle;
180
+ j += 3;
181
+ continue;
182
+ }
183
+ }
184
+ } else {
185
+ if (!inSingle && !inDouble) {
186
+ if (line[j] === '`') {
187
+ let escaped = false;
188
+ let k = j - 1;
189
+ while (k >= 0 && line[k] === '\\') {
190
+ escaped = !escaped;
191
+ k--;
192
+ }
193
+ if (!escaped) {
194
+ inBacktick = !inBacktick;
195
+ }
196
+ }
197
+ }
198
+ if (!inBacktick) {
199
+ if (line[j] === '"' && !inSingle) {
200
+ let escaped = false;
201
+ let k = j - 1;
202
+ while (k >= 0 && line[k] === '\\') {
203
+ escaped = !escaped;
204
+ k--;
205
+ }
206
+ if (!escaped) {
207
+ inDouble = !inDouble;
208
+ }
209
+ }
210
+ else if (line[j] === "'" && !inDouble) {
211
+ let escaped = false;
212
+ let k = j - 1;
213
+ while (k >= 0 && line[k] === '\\') {
214
+ escaped = !escaped;
215
+ k--;
216
+ }
217
+ if (!escaped) {
218
+ inSingle = !inSingle;
219
+ }
220
+ }
221
+ }
222
+ }
223
+ j++;
224
+ }
225
+ if (!isPython) {
226
+ inSingle = false;
227
+ inDouble = false;
228
+ }
229
+ const isEntirelyInsideBlock = isInsideBlockStart && (inBlockJS || inBlockHTML || (commentStartIndex === -1));
230
+ let isPureComment = false;
231
+ if (isEntirelyInsideBlock) {
232
+ isPureComment = true;
233
+ } else if (commentStartIndex !== -1) {
234
+ const beforeComment = line.slice(0, commentStartIndex).trim();
235
+ if (beforeComment === '') {
236
+ isPureComment = true;
237
+ }
238
+ } else if (line.trim() === '') {
239
+ isPureComment = true;
240
+ }
241
+ analysis.push({
242
+ isPureComment,
243
+ commentStartIndex,
244
+ isInsideBlock: isEntirelyInsideBlock || isInsideBlockStart
245
+ });
246
+ }
247
+ return analysis;
248
+ }
249
+
250
+ /** Splicers or removes comments from source data based on the requested mode */
251
+ function spliceComments(data, comments, mode = 'default', ext = '') {
252
+ const hasCRLF = data.includes('\r\n');
253
+ const lineEnding = hasCRLF ? '\r\n' : '\n';
254
+ const originalLines = data.split(/\r?\n/);
255
+ const sortedComments = [...comments].sort((a, b) => b.line - a.line);
256
+ const validComments = sortedComments.filter(c => c.line >= 1 && c.line <= originalLines.length + 1);
257
+
258
+ const annotated = originalLines.map((text, index) => ({ text, originalIndex: index }));
259
+ let analysis = null;
260
+
261
+ if (mode === 'clean') {
262
+ analysis = analyzeComments(originalLines, ext);
263
+ const finalDeletions = new Set();
264
+ for (let i = 0; i < originalLines.length; i++) {
265
+ const lineNum = i + 1;
266
+ if (analysis[i].isPureComment) {
267
+ finalDeletions.add(lineNum);
268
+ } else if (analysis[i].commentStartIndex !== -1) {
269
+ annotated[i].text = originalLines[i].slice(0, analysis[i].commentStartIndex).trimEnd();
270
+ }
271
+ }
272
+
273
+ for (const c of validComments) {
274
+ const lineIdx = c.line - 1;
275
+ if (lineIdx >= 0 && lineIdx < originalLines.length) {
276
+ finalDeletions.add(c.line);
277
+ }
278
+ }
279
+
280
+ const linesToDelete = Array.from(finalDeletions).sort((a, b) => b - a);
281
+
282
+ for (const lineNum of linesToDelete) {
283
+ const targetLine = originalLines[lineNum - 1];
284
+ if (!targetLine) continue;
285
+ const trimmedLine = targetLine.trim();
286
+
287
+ const lineAnalysis = analysis[lineNum - 1];
288
+ const isCommentLine =
289
+ lineAnalysis.isInsideBlock ||
290
+ lineAnalysis.isPureComment ||
291
+ trimmedLine.startsWith('//') ||
292
+ trimmedLine.startsWith('/*') ||
293
+ trimmedLine.startsWith('*') ||
294
+ trimmedLine.startsWith('#') ||
295
+ trimmedLine.startsWith('<!--') ||
296
+ trimmedLine.startsWith('-->') ||
297
+ trimmedLine.startsWith('--') ||
298
+ trimmedLine.endsWith('*/') ||
299
+ trimmedLine === '';
300
+
301
+ if (!isCommentLine) {
302
+ console.warn(`[devsplain] Safety Block: Refused to delete non-comment line ${lineNum}: "${trimmedLine}"`);
303
+ continue;
304
+ }
305
+
306
+ annotated.splice(lineNum - 1, 1);
307
+ }
308
+ } else {
309
+ for (const c of validComments) {
310
+ if (isLineInsideString(originalLines, c.line - 1, ext)) {
311
+ console.warn(`[devsplain] Skipping comment insertion at line ${c.line} to avoid string literal corruption.`);
312
+ continue;
313
+ }
314
+
315
+ const targetLine = originalLines[c.line - 1] || '';
316
+ const indentMatch = targetLine.match(/^([ \t]*)/);
317
+ const indentation = indentMatch ? indentMatch[1] : '';
318
+
319
+ const commentLines = c.comment.split(/\r?\n/).map(line => {
320
+ const trimmed = line.trimStart();
321
+ if (!trimmed) return '';
322
+ if (trimmed.startsWith('*') && !trimmed.startsWith('*/') && !trimmed.startsWith('/*')) {
323
+ return indentation + ' ' + trimmed;
324
+ }
325
+ return indentation + trimmed;
326
+ });
327
+
328
+ const commentObjects = commentLines.map(line => ({ text: line, originalIndex: -1 }));
329
+ annotated.splice(c.line - 1, 0, ...commentObjects);
330
+ }
331
+ }
332
+
333
+ const filtered = annotated.filter(line => line.originalIndex !== -1);
334
+ const filteredText = filtered.map(line => line.text);
335
+ const filteredIndices = filtered.map(line => line.originalIndex);
336
+
337
+ const textEqual = filteredText.every((text, idx) => {
338
+ const origIdx = filteredIndices[idx];
339
+ const originalLine = originalLines[origIdx];
340
+ if (text === originalLine) {
341
+ return true;
342
+ }
343
+ if (mode === 'clean' && analysis) {
344
+ const lineAnalysis = analysis[origIdx];
345
+ if (lineAnalysis && lineAnalysis.commentStartIndex !== -1 && !lineAnalysis.isPureComment) {
346
+ const expectedStripped = originalLine.slice(0, lineAnalysis.commentStartIndex).trimEnd();
347
+ if (text === expectedStripped) {
348
+ return true;
349
+ }
350
+ }
351
+ }
352
+ return false;
353
+ });
354
+
355
+ let indicesSequential = true;
356
+ for (let i = 1; i < filteredIndices.length; i++) {
357
+ if (filteredIndices[i] <= filteredIndices[i - 1]) {
358
+ indicesSequential = false;
359
+ break;
360
+ }
361
+ }
362
+
363
+ if (!textEqual || !indicesSequential) {
364
+ console.error("\nSafety Assertion Failed: Spliced code does not match original code minus comments!");
365
+ process.exit(1);
366
+ }
367
+
368
+ return annotated.map(line => line.text).join(lineEnding);
369
+ }
370
+
371
+ /** Main CLI entry point handler */
372
+ async function runCLI() {
373
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
374
+ askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
375
+
376
+ const args = process.argv.slice(2);
377
+
378
+ if (args.includes('--help') || args.includes('-h')) {
379
+ console.log(`
380
+ devsplain - Universal Polyglot AI Code Commenter
381
+
382
+ Usage:
383
+ devsplain <file-or-directory> [options]
384
+
385
+ Options:
386
+ --light Add ONLY JSDoc/block comments above functions (minimalist)
387
+ --full Add detailed JSDoc/block comments and inline comments
388
+ --dry-run Preview comments without writing to file
389
+ --force Bypass the dirty Git tree safety check
390
+ --provider <name> Override AI provider (gemini, groq, openai, custom)
391
+ --model <name> Override AI model name
392
+ --api-key <key> Override API key for the provider
393
+ --base-url <url> Override base URL for custom APIs
394
+ --config Force run the configuration setup wizard
395
+ --setup-hook Install Git pre-commit and post-commit hooks in repository
396
+ --help, -h Show this help message
397
+ --version, -v Show version information
398
+ `);
399
+ rl.close();
400
+ process.exit(0);
401
+ }
402
+
403
+ if (args.includes('--version') || args.includes('-v')) {
404
+ const pkg = require('../package.json');
405
+ console.log(`devsplain v${pkg.version}`);
406
+ rl.close();
407
+ process.exit(0);
408
+ }
409
+
410
+ if (args.includes('--config')) {
411
+ rl.close();
412
+ await getConfig(true);
413
+ console.log("Success: Configuration updated successfully!");
414
+ process.exit(0);
415
+ }
416
+
417
+ if (args.includes('--setup-hook')) {
418
+ rl.close();
419
+ require('./setup-hook.js');
420
+ return;
421
+ }
422
+
423
+ const getArgValue = (flag) => {
424
+ const index = args.indexOf(flag);
425
+ if (index !== -1 && index + 1 < args.length) {
426
+ return args[index + 1];
427
+ }
428
+ return null;
429
+ };
430
+
431
+ let filepath = '.';
432
+ const flagKeys = ['--provider', '--model', '--api-key', '--base-url'];
433
+ for (let i = 0; i < args.length; i++) {
434
+ const arg = args[i];
435
+ if (arg.startsWith('--')) {
436
+ if (flagKeys.includes(arg)) {
437
+ i++;
438
+ }
439
+ } else {
440
+ filepath = arg;
441
+ break;
442
+ }
443
+ }
444
+
445
+ if (!fs.existsSync(filepath)) {
446
+ console.log(`Error: The path '${filepath}' does not exist.`);
447
+ rl.close();
448
+ process.exit(1);
449
+ }
450
+
451
+ let mode = 'default';
452
+ if (args.includes('--light')) mode = 'light';
453
+ if (args.includes('--full')) mode = 'full';
454
+ if (args.includes('--clean')) mode = 'clean';
455
+ const isDryRun = args.includes('--dry-run');
456
+ const isForce = args.includes('--force');
457
+
458
+ if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
459
+ console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
460
+ rl.close();
461
+ process.exit(1);
462
+ }
463
+
464
+ const config = await getConfig();
465
+
466
+ const cliProvider = getArgValue('--provider');
467
+ const cliModel = getArgValue('--model');
468
+ const cliApiKey = getArgValue('--api-key');
469
+ const cliBaseUrl = getArgValue('--base-url');
470
+
471
+ if (cliProvider) {
472
+ config.provider = cliProvider;
473
+ if (!cliModel) {
474
+ config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile';
475
+ }
476
+ if (!cliBaseUrl) {
477
+ config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : ''));
478
+ }
479
+ }
480
+ if (cliModel) config.model = cliModel;
481
+ if (cliApiKey) config.apiKey = cliApiKey;
482
+ if (cliBaseUrl) config.baseUrl = cliBaseUrl;
483
+
484
+ let successCount = 0;
485
+ let failCount = 0;
486
+
487
+ /** Recursively traverses directories to process files */
488
+ async function processPath(targetPath) {
489
+ const stats = fs.statSync(targetPath);
490
+
491
+ if (stats.isDirectory()) {
492
+ const folderName = path.basename(targetPath);
493
+ const ignoredFolders = [
494
+ 'node_modules', '.git', 'dist', 'build', 'out',
495
+ '.next', '.nuxt', '.svelte-kit',
496
+ 'venv', 'env', '.venv',
497
+ '.vscode', '.idea', 'coverage'
498
+ ];
499
+
500
+ if (ignoredFolders.includes(folderName)) {
501
+ return;
502
+ }
503
+
504
+ console.log(`\n Scanning directory: ${targetPath}`);
505
+ const items = fs.readdirSync(targetPath);
506
+ for (const item of items) {
507
+ const fullPath = path.join(targetPath, item);
508
+ await processPath(fullPath);
509
+ }
510
+ }
511
+ else if (stats.isFile()) {
512
+ const ext = path.extname(targetPath).toLowerCase();
513
+ const validExtensions = [
514
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
515
+ '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
516
+ '.swift', '.kt', '.dart', '.sh'
517
+ ];
518
+
519
+ if (!validExtensions.includes(ext)) {
520
+ return;
521
+ }
522
+
523
+ const filename = path.basename(targetPath);
524
+ const data = fs.readFileSync(targetPath, 'utf-8');
525
+ if (data.trim() === '') {
526
+ console.log(` Skipping ${filename} (Empty File)`);
527
+ return;
528
+ }
529
+
530
+ console.log(` Analyzing ${filename} in ${mode} mode...`);
531
+ try {
532
+ let comments = [];
533
+ let commentedCode;
534
+ if (mode !== 'clean') {
535
+ const cleanData = spliceComments(data, [], 'clean', ext);
536
+ comments = await getComments(cleanData, filename, config, mode);
537
+ commentedCode = spliceComments(cleanData, comments, mode, ext);
538
+ } else {
539
+ commentedCode = spliceComments(data, [], 'clean', ext);
540
+ }
541
+ if (isDryRun) {
542
+ console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
543
+ console.log(commentedCode);
544
+ console.log(`---------------------------------------\n`);
545
+ const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
546
+ if (answer.toLowerCase() === 'write') {
547
+ const tempPath = targetPath + '.tmp';
548
+ fs.writeFileSync(tempPath, commentedCode, 'utf8');
549
+ fs.renameSync(tempPath, targetPath);
550
+ console.log(` Successfully saved ${targetPath}`);
551
+ } else {
552
+ console.log(` Skipped ${targetPath}`);
553
+ }
554
+ } else {
555
+ const tempPath = targetPath + '.tmp';
556
+ fs.writeFileSync(tempPath, commentedCode, 'utf8');
557
+ fs.renameSync(tempPath, targetPath);
558
+ console.log(` Successfully commented ${targetPath}`);
559
+ }
560
+ successCount++;
561
+ } catch (err) {
562
+ console.error(` Error processing ${filename}: ${err.message}`);
563
+ failCount++;
564
+ }
565
+ }
566
+ }
567
+
568
+ await processPath(filepath);
569
+
570
+ if (failCount > 0 && successCount === 0) {
571
+ console.error("\nFailed: No files were successfully commented.");
572
+ rl.close();
573
+ process.exit(1);
574
+ }
575
+
576
+ if (successCount > 0 && failCount > 0) {
577
+ console.log(`\n All done! (Successfully commented: ${successCount}, Failed: ${failCount})`);
578
+ } else {
579
+ console.log("\n All done!");
580
+ }
581
+ rl.close();
582
+ }
583
+
584
+ // If running as a standalone script, start the CLI; otherwise export helpers
585
+ if (require.main === module) {
586
+ runCLI().catch(err => {
587
+ console.error(err);
588
+ process.exit(1);
589
+ });
590
+ } else {
591
+ module.exports = { spliceComments, isLineInsideString };
196
592
  }