devsplain 2.1.0 → 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 +96 -82
- package/lib/llm.js +42 -13
- 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,7 +23,7 @@ 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());
|
|
@@ -116,49 +116,55 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
116
116
|
break; // Ignore rest of line
|
|
117
117
|
}
|
|
118
118
|
} else {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
j
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
133
|
-
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
134
|
-
if (match) {
|
|
135
|
-
cppRawDelimiter = match[1];
|
|
136
|
-
inCppRawString = true;
|
|
137
|
-
j += match[0].length;
|
|
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;
|
|
138
132
|
continue;
|
|
139
133
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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) {
|
|
150
151
|
isRegex = true;
|
|
151
152
|
} else {
|
|
152
|
-
const
|
|
153
|
-
if (/(
|
|
153
|
+
const prevChar = line[k];
|
|
154
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
154
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
|
+
}
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
if (isRegex) {
|
|
164
|
+
inRegex = true;
|
|
165
|
+
j++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
}
|
|
@@ -230,7 +236,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
230
236
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
// Analyzes comments in a given set of code lines [ds]
|
|
234
240
|
function analyzeComments(lines, ext = '') {
|
|
235
241
|
const isPython = ext.toLowerCase() === '.py';
|
|
236
242
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -329,52 +335,59 @@ function analyzeComments(lines, ext = '') {
|
|
|
329
335
|
break;
|
|
330
336
|
}
|
|
331
337
|
} else {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
349
|
-
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
350
|
-
if (match) {
|
|
351
|
-
cppRawDelimiter = match[1];
|
|
352
|
-
inCppRawString = true;
|
|
353
|
-
j += match[0].length;
|
|
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
354
|
continue;
|
|
355
355
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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) {
|
|
366
374
|
isRegex = true;
|
|
367
375
|
} else {
|
|
368
|
-
const
|
|
369
|
-
if (/(
|
|
376
|
+
const prevChar = line[k];
|
|
377
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
370
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
|
+
}
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
386
|
+
if (isRegex) {
|
|
387
|
+
inRegex = true;
|
|
388
|
+
j++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
378
391
|
}
|
|
379
392
|
}
|
|
380
393
|
}
|
|
@@ -464,7 +477,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
464
477
|
return analysis;
|
|
465
478
|
}
|
|
466
479
|
|
|
467
|
-
|
|
480
|
+
// Splices comments into the given code data [ds]
|
|
468
481
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
469
482
|
const hasCRLF = data.includes('\r\n');
|
|
470
483
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -656,7 +669,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
656
669
|
return annotated.map(line => line.text).join(lineEnding);
|
|
657
670
|
}
|
|
658
671
|
|
|
659
|
-
|
|
672
|
+
// Runs the CLI interface for the application [ds]
|
|
660
673
|
async function runCLI() {
|
|
661
674
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
662
675
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -800,7 +813,6 @@ Options:
|
|
|
800
813
|
];
|
|
801
814
|
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
802
815
|
|
|
803
|
-
/** Checks if a path is ignored based on the ignore patterns [ds] */
|
|
804
816
|
function isPathIgnored(targetPath) {
|
|
805
817
|
const filename = path.basename(targetPath);
|
|
806
818
|
for (const pattern of allIgnored) {
|
|
@@ -811,7 +823,7 @@ Options:
|
|
|
811
823
|
return false;
|
|
812
824
|
}
|
|
813
825
|
|
|
814
|
-
|
|
826
|
+
// Processes a given file path, either a file or directory [ds]
|
|
815
827
|
async function processPath(targetPath) {
|
|
816
828
|
const stats = fs.statSync(targetPath);
|
|
817
829
|
|
|
@@ -869,6 +881,7 @@ Options:
|
|
|
869
881
|
fs.renameSync(tempPath, targetPath);
|
|
870
882
|
console.log(` Successfully saved ${targetPath}`);
|
|
871
883
|
} else {
|
|
884
|
+
// Asks the user if they want to save the commented code to a file [ds]
|
|
872
885
|
console.log(` Skipped ${targetPath}`);
|
|
873
886
|
}
|
|
874
887
|
} else {
|
|
@@ -901,6 +914,7 @@ Options:
|
|
|
901
914
|
rl.close();
|
|
902
915
|
}
|
|
903
916
|
|
|
917
|
+
// Starts the CLI application if this script is run directly [ds]
|
|
904
918
|
if (require.main === module) {
|
|
905
919
|
runCLI().catch(err => {
|
|
906
920
|
console.error(err);
|
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,16 +37,44 @@ 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
|
+
// 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
|
+
}
|
|
71
|
+
|
|
72
|
+
// Provide instructions based on the mode [ds]
|
|
73
|
+
let instruction = `Provide block comments above functions and sparse inline comments for complex logic.`;
|
|
45
74
|
if (mode === 'light') {
|
|
46
|
-
instruction =
|
|
75
|
+
instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
|
|
47
76
|
} else if (mode === 'full') {
|
|
48
|
-
instruction =
|
|
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.`;
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
let prompt = `
|
|
@@ -54,24 +83,24 @@ ${instruction}
|
|
|
54
83
|
|
|
55
84
|
CRITICAL RULES:
|
|
56
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.
|
|
57
|
-
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).
|
|
58
87
|
3. Do NOT include the original code in your response.
|
|
59
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.
|
|
60
90
|
|
|
61
91
|
Example Output:
|
|
62
92
|
[
|
|
63
|
-
{ "line": 4, "comment": "
|
|
64
|
-
{ "line": 12, "comment": "
|
|
93
|
+
{ "line": 4, "comment": "${blockExample}" },
|
|
94
|
+
{ "line": 12, "comment": "${inlineExample}" }
|
|
65
95
|
]
|
|
66
96
|
|
|
67
97
|
Here is the source code:
|
|
68
98
|
${numberedCode}
|
|
69
99
|
`.trim();
|
|
70
100
|
|
|
71
|
-
// Initialize text response [ds]
|
|
72
101
|
let textResponse = "";
|
|
73
102
|
|
|
74
|
-
//
|
|
103
|
+
// Handle API requests based on the provider [ds]
|
|
75
104
|
if (config.provider === 'gemini') {
|
|
76
105
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
|
|
77
106
|
let data;
|
|
@@ -94,6 +123,7 @@ ${numberedCode}
|
|
|
94
123
|
}
|
|
95
124
|
textResponse = data.candidates[0].content.parts[0].text;
|
|
96
125
|
} else if (config.provider === 'claude') {
|
|
126
|
+
// Handle Claude API requests [ds]
|
|
97
127
|
const url = `${config.baseUrl}/v1/messages`;
|
|
98
128
|
let data;
|
|
99
129
|
try {
|
|
@@ -122,7 +152,7 @@ ${numberedCode}
|
|
|
122
152
|
}
|
|
123
153
|
textResponse = data.content[0].text;
|
|
124
154
|
}
|
|
125
|
-
//
|
|
155
|
+
// Handle other API requests [ds]
|
|
126
156
|
else {
|
|
127
157
|
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
128
158
|
let data;
|
|
@@ -152,7 +182,6 @@ ${numberedCode}
|
|
|
152
182
|
textResponse = data.choices[0].message.content;
|
|
153
183
|
}
|
|
154
184
|
|
|
155
|
-
// Clean up the text response [ds]
|
|
156
185
|
let cleanText = textResponse.trim();
|
|
157
186
|
const start = cleanText.indexOf('[');
|
|
158
187
|
const end = cleanText.lastIndexOf(']');
|
|
@@ -160,9 +189,9 @@ ${numberedCode}
|
|
|
160
189
|
cleanText = cleanText.substring(start, end + 1);
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
// Parse the response as JSON [ds]
|
|
164
192
|
let parsed;
|
|
165
193
|
try {
|
|
194
|
+
// Attempt to parse the response as JSON [ds]
|
|
166
195
|
parsed = JSON.parse(cleanText);
|
|
167
196
|
} catch (e) {
|
|
168
197
|
throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
|
package/package.json
CHANGED