devsplain 2.0.1 → 2.1.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 +193 -41
- package/lib/config.js +11 -11
- package/lib/llm.js +46 -41
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
let rl;
|
|
11
11
|
let askQuestion;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Checks if the Git working tree is dirty [ds]
|
|
14
14
|
function isGitDirty() {
|
|
15
15
|
try {
|
|
16
16
|
const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
@@ -23,10 +23,13 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Checks if a line of code is inside a string literal [ds]
|
|
27
27
|
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
28
28
|
const isPython = ext.toLowerCase() === '.py';
|
|
29
29
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
30
|
+
const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
|
|
31
|
+
const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
|
|
32
|
+
const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
|
|
30
33
|
let inBacktick = false;
|
|
31
34
|
let inTripleDouble = false;
|
|
32
35
|
let inTripleSingle = false;
|
|
@@ -34,19 +37,55 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
34
37
|
let inDouble = false;
|
|
35
38
|
let inBlockJS = false;
|
|
36
39
|
let inBlockHTML = false;
|
|
40
|
+
let blockDepthJS = 0;
|
|
41
|
+
let inCppRawString = false;
|
|
42
|
+
let cppRawDelimiter = '';
|
|
43
|
+
let inRegex = false;
|
|
37
44
|
for (let i = 0; i < targetLineIndex; i++) {
|
|
38
45
|
const line = lines[i];
|
|
39
46
|
let j = 0;
|
|
40
47
|
while (j < line.length) {
|
|
41
48
|
if (inBlockJS) {
|
|
49
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
50
|
+
if (isRustOrSwift) blockDepthJS++;
|
|
51
|
+
j += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
42
54
|
if (line.slice(j, j + 2) === '*/') {
|
|
43
|
-
|
|
55
|
+
if (isRustOrSwift && blockDepthJS > 1) {
|
|
56
|
+
blockDepthJS--;
|
|
57
|
+
} else {
|
|
58
|
+
inBlockJS = false;
|
|
59
|
+
blockDepthJS = 0;
|
|
60
|
+
}
|
|
44
61
|
j += 2;
|
|
45
62
|
continue;
|
|
46
63
|
}
|
|
47
64
|
j++;
|
|
48
65
|
continue;
|
|
49
66
|
}
|
|
67
|
+
if (inCppRawString) {
|
|
68
|
+
if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
|
|
69
|
+
inCppRawString = false;
|
|
70
|
+
j += 2 + cppRawDelimiter.length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
j++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (inRegex) {
|
|
77
|
+
let escaped = false;
|
|
78
|
+
let k = j - 1;
|
|
79
|
+
while (k >= 0 && line[k] === '\\') {
|
|
80
|
+
escaped = !escaped;
|
|
81
|
+
k--;
|
|
82
|
+
}
|
|
83
|
+
if (line[j] === '/' && !escaped) {
|
|
84
|
+
inRegex = false;
|
|
85
|
+
}
|
|
86
|
+
j++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
50
89
|
if (inBlockHTML) {
|
|
51
90
|
if (line.slice(j, j + 3) === '-->') {
|
|
52
91
|
inBlockHTML = false;
|
|
@@ -65,7 +104,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
65
104
|
} else if (isHTML) {
|
|
66
105
|
if (line.slice(j, j + 4) === '<!--') {
|
|
67
106
|
inBlockHTML = true;
|
|
68
|
-
// Check if current character is a backtick [ds]
|
|
69
107
|
j += 4;
|
|
70
108
|
continue;
|
|
71
109
|
}
|
|
@@ -78,17 +116,56 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
78
116
|
break; // Ignore rest of line
|
|
79
117
|
}
|
|
80
118
|
} else {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
|
|
120
|
+
if (isShellOrRuby) {
|
|
121
|
+
if (line[j] === '#') {
|
|
122
|
+
break; // Ignore rest of line
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (line.slice(j, j + 2) === '//') {
|
|
126
|
+
break; // Ignore rest of line
|
|
127
|
+
}
|
|
128
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
129
|
+
inBlockJS = true;
|
|
130
|
+
blockDepthJS = 1;
|
|
131
|
+
j += 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (ext.toLowerCase() === '.php' && line[j] === '#') {
|
|
135
|
+
break; // Ignore rest of line
|
|
136
|
+
}
|
|
137
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
138
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
139
|
+
if (match) {
|
|
140
|
+
cppRawDelimiter = match[1];
|
|
141
|
+
inCppRawString = true;
|
|
142
|
+
j += match[0].length;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (isJS && line[j] === '/') {
|
|
147
|
+
let k = j - 1;
|
|
148
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
149
|
+
let isRegex = false;
|
|
150
|
+
if (k < 0) {
|
|
151
|
+
isRegex = true;
|
|
152
|
+
} else {
|
|
153
|
+
const prevChar = line[k];
|
|
154
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
155
|
+
isRegex = true;
|
|
156
|
+
} else {
|
|
157
|
+
const prefix = line.slice(0, k + 1);
|
|
158
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
159
|
+
isRegex = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isRegex) {
|
|
164
|
+
inRegex = true;
|
|
165
|
+
j++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
92
169
|
}
|
|
93
170
|
}
|
|
94
171
|
}
|
|
@@ -125,7 +202,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
125
202
|
}
|
|
126
203
|
}
|
|
127
204
|
if (!inBacktick) {
|
|
128
|
-
// Check if current character is a double quote [ds]
|
|
129
205
|
if (line[j] === '"' && !inSingle) {
|
|
130
206
|
let escaped = false;
|
|
131
207
|
let k = j - 1;
|
|
@@ -157,13 +233,16 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
157
233
|
inDouble = false;
|
|
158
234
|
}
|
|
159
235
|
}
|
|
160
|
-
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
236
|
+
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
|
|
161
237
|
}
|
|
162
238
|
|
|
163
|
-
|
|
239
|
+
// Analyzes comments in a given set of code lines [ds]
|
|
164
240
|
function analyzeComments(lines, ext = '') {
|
|
165
241
|
const isPython = ext.toLowerCase() === '.py';
|
|
166
242
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
243
|
+
const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
|
|
244
|
+
const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
|
|
245
|
+
const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
|
|
167
246
|
const analysis = [];
|
|
168
247
|
let inBacktick = false;
|
|
169
248
|
let inTripleDouble = false;
|
|
@@ -172,6 +251,10 @@ function analyzeComments(lines, ext = '') {
|
|
|
172
251
|
let inDouble = false;
|
|
173
252
|
let inBlockJS = false;
|
|
174
253
|
let inBlockHTML = false;
|
|
254
|
+
let blockDepthJS = 0;
|
|
255
|
+
let inCppRawString = false;
|
|
256
|
+
let cppRawDelimiter = '';
|
|
257
|
+
let inRegex = false;
|
|
175
258
|
for (let i = 0; i < lines.length; i++) {
|
|
176
259
|
const line = lines[i];
|
|
177
260
|
let commentStartIndex = -1;
|
|
@@ -179,14 +262,46 @@ function analyzeComments(lines, ext = '') {
|
|
|
179
262
|
let j = 0;
|
|
180
263
|
while (j < line.length) {
|
|
181
264
|
if (inBlockJS) {
|
|
265
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
266
|
+
if (isRustOrSwift) blockDepthJS++;
|
|
267
|
+
j += 2;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
182
270
|
if (line.slice(j, j + 2) === '*/') {
|
|
183
|
-
|
|
271
|
+
if (isRustOrSwift && blockDepthJS > 1) {
|
|
272
|
+
blockDepthJS--;
|
|
273
|
+
} else {
|
|
274
|
+
inBlockJS = false;
|
|
275
|
+
blockDepthJS = 0;
|
|
276
|
+
}
|
|
184
277
|
j += 2;
|
|
185
278
|
continue;
|
|
186
279
|
}
|
|
187
280
|
j++;
|
|
188
281
|
continue;
|
|
189
282
|
}
|
|
283
|
+
if (inCppRawString) {
|
|
284
|
+
if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
|
|
285
|
+
inCppRawString = false;
|
|
286
|
+
j += 2 + cppRawDelimiter.length;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
j++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (inRegex) {
|
|
293
|
+
let escaped = false;
|
|
294
|
+
let k = j - 1;
|
|
295
|
+
while (k >= 0 && line[k] === '\\') {
|
|
296
|
+
escaped = !escaped;
|
|
297
|
+
k--;
|
|
298
|
+
}
|
|
299
|
+
if (line[j] === '/' && !escaped) {
|
|
300
|
+
inRegex = false;
|
|
301
|
+
}
|
|
302
|
+
j++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
190
305
|
if (inBlockHTML) {
|
|
191
306
|
if (line.slice(j, j + 3) === '-->') {
|
|
192
307
|
inBlockHTML = false;
|
|
@@ -220,20 +335,60 @@ function analyzeComments(lines, ext = '') {
|
|
|
220
335
|
break;
|
|
221
336
|
}
|
|
222
337
|
} else {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
j
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
338
|
+
const isShellOrRuby = ['.sh', '.rb'].includes(ext.toLowerCase());
|
|
339
|
+
if (isShellOrRuby) {
|
|
340
|
+
if (line[j] === '#') {
|
|
341
|
+
commentStartIndex = j;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
if (line.slice(j, j + 2) === '//') {
|
|
346
|
+
commentStartIndex = j;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
350
|
+
commentStartIndex = j;
|
|
351
|
+
inBlockJS = true;
|
|
352
|
+
blockDepthJS = 1;
|
|
353
|
+
j += 2;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (ext.toLowerCase() === '.php' && line[j] === '#') {
|
|
357
|
+
commentStartIndex = j;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
361
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
362
|
+
if (match) {
|
|
363
|
+
cppRawDelimiter = match[1];
|
|
364
|
+
inCppRawString = true;
|
|
365
|
+
j += match[0].length;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (isJS && line[j] === '/') {
|
|
370
|
+
let k = j - 1;
|
|
371
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
372
|
+
let isRegex = false;
|
|
373
|
+
if (k < 0) {
|
|
374
|
+
isRegex = true;
|
|
375
|
+
} else {
|
|
376
|
+
const prevChar = line[k];
|
|
377
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
378
|
+
isRegex = true;
|
|
379
|
+
} else {
|
|
380
|
+
const prefix = line.slice(0, k + 1);
|
|
381
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
382
|
+
isRegex = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (isRegex) {
|
|
387
|
+
inRegex = true;
|
|
388
|
+
j++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
237
392
|
}
|
|
238
393
|
}
|
|
239
394
|
}
|
|
@@ -281,7 +436,6 @@ function analyzeComments(lines, ext = '') {
|
|
|
281
436
|
inDouble = !inDouble;
|
|
282
437
|
}
|
|
283
438
|
}
|
|
284
|
-
// Check if current character is a single quote [ds]
|
|
285
439
|
else if (line[j] === "'" && !inDouble) {
|
|
286
440
|
let escaped = false;
|
|
287
441
|
let k = j - 1;
|
|
@@ -323,7 +477,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
323
477
|
return analysis;
|
|
324
478
|
}
|
|
325
479
|
|
|
326
|
-
|
|
480
|
+
// Splices comments into the given code data [ds]
|
|
327
481
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
328
482
|
const hasCRLF = data.includes('\r\n');
|
|
329
483
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -372,7 +526,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
372
526
|
continue;
|
|
373
527
|
}
|
|
374
528
|
|
|
375
|
-
// Prune mode: remove all comments from the line [ds]
|
|
376
529
|
if (mode === 'prune') {
|
|
377
530
|
if (lineAnalysis.isPureComment) {
|
|
378
531
|
finalDeletions.add(lineNum);
|
|
@@ -447,7 +600,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
447
600
|
const indentation = indentMatch ? indentMatch[1] : '';
|
|
448
601
|
|
|
449
602
|
const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
|
|
450
|
-
// Add the comment to the list of comments [ds]
|
|
451
603
|
let trimmed = line.trimStart();
|
|
452
604
|
if (!trimmed) return '';
|
|
453
605
|
|
|
@@ -479,7 +631,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
479
631
|
const filteredText = filtered.map(line => line.text);
|
|
480
632
|
const filteredIndices = filtered.map(line => line.originalIndex);
|
|
481
633
|
|
|
482
|
-
// Check if the filtered text is equal to the original text [ds]
|
|
483
634
|
const textEqual = filteredText.every((text, idx) => {
|
|
484
635
|
const origIdx = filteredIndices[idx];
|
|
485
636
|
const originalLine = originalLines[origIdx];
|
|
@@ -518,7 +669,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
518
669
|
return annotated.map(line => line.text).join(lineEnding);
|
|
519
670
|
}
|
|
520
671
|
|
|
521
|
-
|
|
672
|
+
// Runs the CLI interface for the application [ds]
|
|
522
673
|
async function runCLI() {
|
|
523
674
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
524
675
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -662,7 +813,6 @@ Options:
|
|
|
662
813
|
];
|
|
663
814
|
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
664
815
|
|
|
665
|
-
/** Checks if a given path is ignored [ds] */
|
|
666
816
|
function isPathIgnored(targetPath) {
|
|
667
817
|
const filename = path.basename(targetPath);
|
|
668
818
|
for (const pattern of allIgnored) {
|
|
@@ -673,7 +823,7 @@ Options:
|
|
|
673
823
|
return false;
|
|
674
824
|
}
|
|
675
825
|
|
|
676
|
-
|
|
826
|
+
// Processes a given file path, either a file or directory [ds]
|
|
677
827
|
async function processPath(targetPath) {
|
|
678
828
|
const stats = fs.statSync(targetPath);
|
|
679
829
|
|
|
@@ -731,6 +881,7 @@ Options:
|
|
|
731
881
|
fs.renameSync(tempPath, targetPath);
|
|
732
882
|
console.log(` Successfully saved ${targetPath}`);
|
|
733
883
|
} else {
|
|
884
|
+
// Asks the user if they want to save the commented code to a file [ds]
|
|
734
885
|
console.log(` Skipped ${targetPath}`);
|
|
735
886
|
}
|
|
736
887
|
} else {
|
|
@@ -763,6 +914,7 @@ Options:
|
|
|
763
914
|
rl.close();
|
|
764
915
|
}
|
|
765
916
|
|
|
917
|
+
// Starts the CLI application if this script is run directly [ds]
|
|
766
918
|
if (require.main === module) {
|
|
767
919
|
runCLI().catch(err => {
|
|
768
920
|
console.error(err);
|
package/lib/config.js
CHANGED
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
const configPath = path.join(os.homedir(), '.devsplainrc');
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Asks the user for a secret input (e.g., an API key) without echoing the input to the console. [ds] */
|
|
8
8
|
function askSecret(query) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
10
|
const rl = readline.createInterface({
|
|
@@ -33,7 +33,7 @@ function askSecret(query) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/** Attempts to fetch the configuration settings, either from environment variables or by prompting the user. [ds] */
|
|
37
37
|
async function getConfig(forceWizard = false) {
|
|
38
38
|
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
39
39
|
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
@@ -46,8 +46,8 @@ async function getConfig(forceWizard = false) {
|
|
|
46
46
|
baseUrl
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
// If the configuration file doesn't exist, or if we're forcing the wizard, let's prompt the user for configuration. [ds]
|
|
49
50
|
|
|
50
|
-
// If the configuration file does not exist or forceWizard is true, prompt the user to configure [ds]
|
|
51
51
|
if (!fs.existsSync(configPath) || forceWizard) {
|
|
52
52
|
let rl = readline.createInterface({
|
|
53
53
|
input: process.stdin,
|
|
@@ -58,13 +58,13 @@ async function getConfig(forceWizard = false) {
|
|
|
58
58
|
let config = null;
|
|
59
59
|
let confirmed = false;
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Keep prompting the user until they confirm their configuration settings. [ds]
|
|
62
62
|
while (!confirmed) {
|
|
63
63
|
let baseUrl = "";
|
|
64
64
|
let model = "";
|
|
65
65
|
let provider = "";
|
|
66
66
|
|
|
67
|
-
// Display the list of available AI providers [ds]
|
|
67
|
+
// Display the list of available AI providers to the user. [ds]
|
|
68
68
|
console.log("\nWhich AI Provider Do You want to use?");
|
|
69
69
|
console.log("1. Groq (Free, Fast, Llama-3)");
|
|
70
70
|
console.log("2. Gemini (Free Tier)");
|
|
@@ -115,7 +115,7 @@ async function getConfig(forceWizard = false) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// Get the API key from the user. [ds]
|
|
119
119
|
let apiKey = '';
|
|
120
120
|
while (true) {
|
|
121
121
|
const promptMsg = provider === 'custom'
|
|
@@ -142,7 +142,7 @@ async function getConfig(forceWizard = false) {
|
|
|
142
142
|
console.log(`API key is required for provider '${provider}'.`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Ask the user if they want to enable auto-pruning of existing comments [ds]
|
|
145
|
+
// Ask the user if they want to enable auto-pruning of existing human/AI comments. [ds]
|
|
146
146
|
let autoPrune = false;
|
|
147
147
|
while (true) {
|
|
148
148
|
const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
|
|
@@ -156,7 +156,7 @@ async function getConfig(forceWizard = false) {
|
|
|
156
156
|
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
// Display a summary of the configuration [ds]
|
|
159
|
+
// Display a summary of the user's configuration settings. [ds]
|
|
160
160
|
console.log("\n--- Configuration Summary ---");
|
|
161
161
|
console.log(`Provider: ${provider}`);
|
|
162
162
|
console.log(`Model: ${model}`);
|
|
@@ -165,7 +165,7 @@ async function getConfig(forceWizard = false) {
|
|
|
165
165
|
console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
|
|
166
166
|
console.log("-----------------------------\n");
|
|
167
167
|
|
|
168
|
-
// Confirm the configuration with the user [ds]
|
|
168
|
+
// Confirm the configuration settings with the user. [ds]
|
|
169
169
|
while (true) {
|
|
170
170
|
const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
|
|
171
171
|
if (confirm === '' || confirm === 'y' || confirm === 'yes') {
|
|
@@ -187,10 +187,9 @@ async function getConfig(forceWizard = false) {
|
|
|
187
187
|
|
|
188
188
|
rl.close();
|
|
189
189
|
|
|
190
|
-
// Write the configuration to the config file [ds]
|
|
190
|
+
// Write the configuration settings to the config file. [ds]
|
|
191
191
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
192
192
|
try {
|
|
193
|
-
// Set the permissions of the config file to prevent other users from reading it [ds]
|
|
194
193
|
if (process.platform !== 'win32') {
|
|
195
194
|
fs.chmodSync(configPath, 0o600);
|
|
196
195
|
}
|
|
@@ -204,4 +203,5 @@ async function getConfig(forceWizard = false) {
|
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
|
|
206
|
+
// Export the getConfig function for use in other modules. [ds]
|
|
207
207
|
module.exports = { getConfig };
|
package/lib/llm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Asynchronously fetches data from the specified URL with a retry mechanism [ds]
|
|
2
2
|
async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
|
|
3
3
|
let lastError;
|
|
4
|
+
// Loop through each attempt, up to the maximum number of retries [ds]
|
|
4
5
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
5
6
|
const controller = new AbortController();
|
|
6
7
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
@@ -36,66 +37,70 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
|
|
|
36
37
|
throw lastError;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Retrieves comments for the given code, language, and configuration [ds]
|
|
40
41
|
async function getComments(code, language, config, mode = 'default') {
|
|
41
42
|
const lines = code.split(/\r?\n/);
|
|
42
43
|
const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
// Extract the file extension from the language string [ds]
|
|
46
|
+
const extMatch = language.match(/\.[0-9a-z]+$/i);
|
|
47
|
+
const ext = extMatch ? extMatch[0].toLowerCase() : '';
|
|
48
|
+
const isPython = ext === '.py';
|
|
49
|
+
const isRubyOrShell = ['.rb', '.sh', '.php'].includes(ext);
|
|
50
|
+
const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
|
|
51
|
+
const isSql = ext === '.sql';
|
|
52
|
+
|
|
53
|
+
// Define the single-line comment token and examples [ds]
|
|
54
|
+
let singleLineToken = '//';
|
|
55
|
+
let blockExample = '/** Calculates the total price */';
|
|
56
|
+
let inlineExample = '// Check for null values';
|
|
57
|
+
|
|
58
|
+
if (isPython || isRubyOrShell) {
|
|
59
|
+
singleLineToken = '#';
|
|
60
|
+
blockExample = '# Calculates the total price';
|
|
61
|
+
inlineExample = '# Check for null values';
|
|
62
|
+
} else if (isHTML) {
|
|
63
|
+
singleLineToken = '<!--';
|
|
64
|
+
blockExample = '<!-- Calculates the total price -->';
|
|
65
|
+
inlineExample = '<!-- Check for null values -->';
|
|
66
|
+
} else if (isSql) {
|
|
67
|
+
singleLineToken = '--';
|
|
68
|
+
blockExample = '-- Calculates the total price';
|
|
69
|
+
inlineExample = '-- Check for null values';
|
|
70
|
+
}
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} else if (mode === 'full') {
|
|
71
|
-
instruction = "Provide highly detailed JSDoc/docstrings above functions, and exhaustive step-by-step inline comments (using standard comment syntax like // or #) explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.";
|
|
72
|
-
}
|
|
72
|
+
// Provide instructions based on the mode [ds]
|
|
73
|
+
let instruction = `Provide block comments above functions and sparse inline comments for complex logic.`;
|
|
74
|
+
if (mode === 'light') {
|
|
75
|
+
instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
|
|
76
|
+
} else if (mode === 'full') {
|
|
77
|
+
instruction = `Provide highly detailed block comments above functions, and exhaustive step-by-step inline comments explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.`;
|
|
78
|
+
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
let prompt = `
|
|
75
81
|
You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
|
|
76
82
|
${instruction}
|
|
77
83
|
|
|
78
84
|
CRITICAL RULES:
|
|
79
85
|
1. You MUST respond with ONLY a raw, valid JSON array of objects. NO markdown formatting, NO backticks, NO explanations, NO text before or after the JSON.
|
|
80
|
-
2. Each object must have exactly two properties: "line" (the integer line number where the comment should be inserted ABOVE) and "comment" (the text of the comment itself
|
|
86
|
+
2. Each object must have exactly two properties: "line" (the integer line number where the comment should be inserted ABOVE) and "comment" (the text of the comment itself).
|
|
81
87
|
3. Do NOT include the original code in your response.
|
|
82
88
|
4. If no comments are needed, return an empty array: [].
|
|
89
|
+
5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.
|
|
83
90
|
|
|
84
91
|
Example Output:
|
|
85
92
|
[
|
|
86
|
-
{ "line": 4, "comment": "
|
|
87
|
-
{ "line": 12, "comment": "
|
|
93
|
+
{ "line": 4, "comment": "${blockExample}" },
|
|
94
|
+
{ "line": 12, "comment": "${inlineExample}" }
|
|
88
95
|
]
|
|
89
96
|
|
|
90
97
|
Here is the source code:
|
|
91
98
|
${numberedCode}
|
|
92
|
-
|
|
93
|
-
}
|
|
99
|
+
`.trim();
|
|
94
100
|
|
|
95
|
-
// Initialize text response [ds]
|
|
96
101
|
let textResponse = "";
|
|
97
102
|
|
|
98
|
-
//
|
|
103
|
+
// Handle API requests based on the provider [ds]
|
|
99
104
|
if (config.provider === 'gemini') {
|
|
100
105
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
|
|
101
106
|
let data;
|
|
@@ -118,6 +123,7 @@ ${numberedCode}
|
|
|
118
123
|
}
|
|
119
124
|
textResponse = data.candidates[0].content.parts[0].text;
|
|
120
125
|
} else if (config.provider === 'claude') {
|
|
126
|
+
// Handle Claude API requests [ds]
|
|
121
127
|
const url = `${config.baseUrl}/v1/messages`;
|
|
122
128
|
let data;
|
|
123
129
|
try {
|
|
@@ -146,7 +152,7 @@ ${numberedCode}
|
|
|
146
152
|
}
|
|
147
153
|
textResponse = data.content[0].text;
|
|
148
154
|
}
|
|
149
|
-
//
|
|
155
|
+
// Handle other API requests [ds]
|
|
150
156
|
else {
|
|
151
157
|
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
152
158
|
let data;
|
|
@@ -176,7 +182,6 @@ ${numberedCode}
|
|
|
176
182
|
textResponse = data.choices[0].message.content;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
// Clean up the text response [ds]
|
|
180
185
|
let cleanText = textResponse.trim();
|
|
181
186
|
const start = cleanText.indexOf('[');
|
|
182
187
|
const end = cleanText.lastIndexOf(']');
|
|
@@ -184,9 +189,9 @@ ${numberedCode}
|
|
|
184
189
|
cleanText = cleanText.substring(start, end + 1);
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
// Parse the response as JSON [ds]
|
|
188
192
|
let parsed;
|
|
189
193
|
try {
|
|
194
|
+
// Attempt to parse the response as JSON [ds]
|
|
190
195
|
parsed = JSON.parse(cleanText);
|
|
191
196
|
} catch (e) {
|
|
192
197
|
throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
|
package/package.json
CHANGED