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.
Files changed (3) hide show
  1. package/bin/cli.js +96 -82
  2. package/lib/llm.js +42 -13
  3. 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
- /** Checks if the Git repository has uncommitted changes [ds] */
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
- /** Checks if a line of code is inside a string literal [ds] */
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
- if (line.slice(j, j + 2) === '//') {
120
- break; // Ignore rest of line
121
- }
122
- if (line.slice(j, j + 2) === '/*') {
123
- inBlockJS = true;
124
- blockDepthJS = 1;
125
- j += 2;
126
- continue;
127
- }
128
- const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
129
- if (isShellOrRuby && line[j] === '#') {
130
- break; // Ignore rest of line
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
- if (isJS && line[j] === '/') {
142
- let k = j - 1;
143
- while (k >= 0 && /\s/.test(line[k])) k--;
144
- let isRegex = false;
145
- if (k < 0) {
146
- isRegex = true;
147
- } else {
148
- const prevChar = line[k];
149
- if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
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 prefix = line.slice(0, k + 1);
153
- if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
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
- if (isRegex) {
159
- inRegex = true;
160
- j++;
161
- continue;
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
- /** Analyzes the comments in a given set of code lines [ds] */
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
- if (line.slice(j, j + 2) === '//') {
333
- commentStartIndex = j;
334
- break;
335
- }
336
- if (line.slice(j, j + 2) === '/*') {
337
- commentStartIndex = j;
338
- inBlockJS = true;
339
- blockDepthJS = 1;
340
- j += 2;
341
- continue;
342
- }
343
- const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
344
- if (isShellOrRuby && line[j] === '#') {
345
- commentStartIndex = j;
346
- break;
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
- if (isJS && line[j] === '/') {
358
- let k = j - 1;
359
- while (k >= 0 && /\s/.test(line[k])) k--;
360
- let isRegex = false;
361
- if (k < 0) {
362
- isRegex = true;
363
- } else {
364
- const prevChar = line[k];
365
- if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
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 prefix = line.slice(0, k + 1);
369
- if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
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
- if (isRegex) {
375
- inRegex = true;
376
- j++;
377
- continue;
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
- /** Splices comments into the provided code data [ds] */
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
- /** Runs the CLI application [ds] */
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
- /** Processes a given path, which can be a file or directory [ds] */
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
- /** Fetches a URL with retry functionality [ds] */
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
- /** Gets comments for the given code [ds] */
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
- let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
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 = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
75
+ instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
47
76
  } else if (mode === 'full') {
48
- 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.";
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, including standard comment syntax like // or /** */).
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": "/** Calculates the total price */" },
64
- { "line": 12, "comment": "// Check for null values" }
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
- // Check if provider is Gemini [ds]
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
- // Otherwise, use an OpenAI-compatible provider [ds]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
5
  "author": "mwahaj36",
6
6
  "license": "MIT",