devsplain 2.0.1 → 2.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.
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 is dirty [ds] */
13
+ /** Checks if the Git repository has uncommitted changes [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
- /** Checks if a line of code is inside a string [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());
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
- inBlockJS = false;
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
  }
@@ -83,6 +121,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
83
121
  }
84
122
  if (line.slice(j, j + 2) === '/*') {
85
123
  inBlockJS = true;
124
+ blockDepthJS = 1;
86
125
  j += 2;
87
126
  continue;
88
127
  }
@@ -90,6 +129,38 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
90
129
  if (isShellOrRuby && line[j] === '#') {
91
130
  break; // Ignore rest of line
92
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;
138
+ continue;
139
+ }
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)) {
150
+ isRegex = true;
151
+ } else {
152
+ const prefix = line.slice(0, k + 1);
153
+ if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
154
+ isRegex = true;
155
+ }
156
+ }
157
+ }
158
+ if (isRegex) {
159
+ inRegex = true;
160
+ j++;
161
+ continue;
162
+ }
163
+ }
93
164
  }
94
165
  }
95
166
  if (isPython) {
@@ -125,7 +196,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
125
196
  }
126
197
  }
127
198
  if (!inBacktick) {
128
- // Check if current character is a double quote [ds]
129
199
  if (line[j] === '"' && !inSingle) {
130
200
  let escaped = false;
131
201
  let k = j - 1;
@@ -157,13 +227,16 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
157
227
  inDouble = false;
158
228
  }
159
229
  }
160
- return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
230
+ return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
161
231
  }
162
232
 
163
- /** Analyzes comments in a given list of lines [ds] */
233
+ /** Analyzes the comments in a given set of code lines [ds] */
164
234
  function analyzeComments(lines, ext = '') {
165
235
  const isPython = ext.toLowerCase() === '.py';
166
236
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
237
+ const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
238
+ const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
239
+ const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
167
240
  const analysis = [];
168
241
  let inBacktick = false;
169
242
  let inTripleDouble = false;
@@ -172,6 +245,10 @@ function analyzeComments(lines, ext = '') {
172
245
  let inDouble = false;
173
246
  let inBlockJS = false;
174
247
  let inBlockHTML = false;
248
+ let blockDepthJS = 0;
249
+ let inCppRawString = false;
250
+ let cppRawDelimiter = '';
251
+ let inRegex = false;
175
252
  for (let i = 0; i < lines.length; i++) {
176
253
  const line = lines[i];
177
254
  let commentStartIndex = -1;
@@ -179,14 +256,46 @@ function analyzeComments(lines, ext = '') {
179
256
  let j = 0;
180
257
  while (j < line.length) {
181
258
  if (inBlockJS) {
259
+ if (line.slice(j, j + 2) === '/*') {
260
+ if (isRustOrSwift) blockDepthJS++;
261
+ j += 2;
262
+ continue;
263
+ }
182
264
  if (line.slice(j, j + 2) === '*/') {
183
- inBlockJS = false;
265
+ if (isRustOrSwift && blockDepthJS > 1) {
266
+ blockDepthJS--;
267
+ } else {
268
+ inBlockJS = false;
269
+ blockDepthJS = 0;
270
+ }
184
271
  j += 2;
185
272
  continue;
186
273
  }
187
274
  j++;
188
275
  continue;
189
276
  }
277
+ if (inCppRawString) {
278
+ if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
279
+ inCppRawString = false;
280
+ j += 2 + cppRawDelimiter.length;
281
+ continue;
282
+ }
283
+ j++;
284
+ continue;
285
+ }
286
+ if (inRegex) {
287
+ let escaped = false;
288
+ let k = j - 1;
289
+ while (k >= 0 && line[k] === '\\') {
290
+ escaped = !escaped;
291
+ k--;
292
+ }
293
+ if (line[j] === '/' && !escaped) {
294
+ inRegex = false;
295
+ }
296
+ j++;
297
+ continue;
298
+ }
190
299
  if (inBlockHTML) {
191
300
  if (line.slice(j, j + 3) === '-->') {
192
301
  inBlockHTML = false;
@@ -227,6 +336,7 @@ function analyzeComments(lines, ext = '') {
227
336
  if (line.slice(j, j + 2) === '/*') {
228
337
  commentStartIndex = j;
229
338
  inBlockJS = true;
339
+ blockDepthJS = 1;
230
340
  j += 2;
231
341
  continue;
232
342
  }
@@ -235,6 +345,38 @@ function analyzeComments(lines, ext = '') {
235
345
  commentStartIndex = j;
236
346
  break;
237
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;
354
+ continue;
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)) {
366
+ isRegex = true;
367
+ } else {
368
+ const prefix = line.slice(0, k + 1);
369
+ if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
370
+ isRegex = true;
371
+ }
372
+ }
373
+ }
374
+ if (isRegex) {
375
+ inRegex = true;
376
+ j++;
377
+ continue;
378
+ }
379
+ }
238
380
  }
239
381
  }
240
382
  if (isPython) {
@@ -281,7 +423,6 @@ function analyzeComments(lines, ext = '') {
281
423
  inDouble = !inDouble;
282
424
  }
283
425
  }
284
- // Check if current character is a single quote [ds]
285
426
  else if (line[j] === "'" && !inDouble) {
286
427
  let escaped = false;
287
428
  let k = j - 1;
@@ -323,7 +464,7 @@ function analyzeComments(lines, ext = '') {
323
464
  return analysis;
324
465
  }
325
466
 
326
- /** Splices comments into the given data [ds] */
467
+ /** Splices comments into the provided code data [ds] */
327
468
  function spliceComments(data, comments, mode = 'default', ext = '') {
328
469
  const hasCRLF = data.includes('\r\n');
329
470
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -372,7 +513,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
372
513
  continue;
373
514
  }
374
515
 
375
- // Prune mode: remove all comments from the line [ds]
376
516
  if (mode === 'prune') {
377
517
  if (lineAnalysis.isPureComment) {
378
518
  finalDeletions.add(lineNum);
@@ -447,7 +587,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
447
587
  const indentation = indentMatch ? indentMatch[1] : '';
448
588
 
449
589
  const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
450
- // Add the comment to the list of comments [ds]
451
590
  let trimmed = line.trimStart();
452
591
  if (!trimmed) return '';
453
592
 
@@ -479,7 +618,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
479
618
  const filteredText = filtered.map(line => line.text);
480
619
  const filteredIndices = filtered.map(line => line.originalIndex);
481
620
 
482
- // Check if the filtered text is equal to the original text [ds]
483
621
  const textEqual = filteredText.every((text, idx) => {
484
622
  const origIdx = filteredIndices[idx];
485
623
  const originalLine = originalLines[origIdx];
@@ -518,7 +656,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
518
656
  return annotated.map(line => line.text).join(lineEnding);
519
657
  }
520
658
 
521
- /** Runs the CLI interface [ds] */
659
+ /** Runs the CLI application [ds] */
522
660
  async function runCLI() {
523
661
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
524
662
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -662,7 +800,7 @@ Options:
662
800
  ];
663
801
  const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
664
802
 
665
- /** Checks if a given path is ignored [ds] */
803
+ /** Checks if a path is ignored based on the ignore patterns [ds] */
666
804
  function isPathIgnored(targetPath) {
667
805
  const filename = path.basename(targetPath);
668
806
  for (const pattern of allIgnored) {
@@ -673,7 +811,7 @@ Options:
673
811
  return false;
674
812
  }
675
813
 
676
- /** Processes a given path [ds] */
814
+ /** Processes a given path, which can be a file or directory [ds] */
677
815
  async function processPath(targetPath) {
678
816
  const stats = fs.statSync(targetPath);
679
817
 
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
- /** Prompts the user for a secret input, hiding the input from the console [ds] */
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
- /** Retrieves the configuration, either from environment variables or by prompting the user [ds] */
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
- // Continuously prompt the user until the configuration is confirmed [ds]
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
- // Prompt the user for an API key [ds]
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
@@ -41,37 +41,14 @@ async function getComments(code, language, config, mode = 'default') {
41
41
  const lines = code.split(/\r?\n/);
42
42
  const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
43
43
 
44
- let prompt = "";
45
- if (mode === 'clean') {
46
- prompt = `
47
- You are a code documentation scrubber. Analyze the following ${language} code which has line numbers prepended to it.
48
- Your goal is to identify all lines containing comments (both block/JSDoc and inline comments) and return their line numbers to delete them.
49
-
50
- CRITICAL RULES:
51
- 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.
52
- 2. Each object must have exactly two properties: "line" (the integer line number of the comment line to delete) and "action" (which must be the string "delete").
53
- 3. Do NOT include the original code in your response.
54
- 4. If no comments are found, return an empty array: [].
55
- 5. For block or JSDoc comments (e.g., starting with /* and ending with */), you MUST identify and return the line numbers of ALL lines in that block, including the opening /*, all intermediate lines, and the closing */. Do NOT leave trailing comment delimiters behind.
56
-
57
- Example Output:
58
- [
59
- { "line": 4, "action": "delete" },
60
- { "line": 5, "action": "delete" }
61
- ]
62
-
63
- Here is the source code:
64
- ${numberedCode}
65
- `.trim();
66
- } else {
67
- let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
68
- if (mode === 'light') {
69
- instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
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
- }
44
+ let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
45
+ if (mode === 'light') {
46
+ instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
47
+ } 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.";
49
+ }
73
50
 
74
- prompt = `
51
+ let prompt = `
75
52
  You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
76
53
  ${instruction}
77
54
 
@@ -89,8 +66,7 @@ Example Output:
89
66
 
90
67
  Here is the source code:
91
68
  ${numberedCode}
92
- `.trim();
93
- }
69
+ `.trim();
94
70
 
95
71
  // Initialize text response [ds]
96
72
  let textResponse = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
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",