devsplain 2.2.2 → 2.2.3

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
@@ -231,6 +231,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
231
231
  if (resetsAtLineEnd) {
232
232
  inSingle = false;
233
233
  inDouble = false;
234
+ inRegex = false;
234
235
  }
235
236
  }
236
237
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
@@ -455,6 +456,7 @@ function analyzeComments(lines, ext = '') {
455
456
  if (resetsAtLineEnd) {
456
457
  inSingle = false;
457
458
  inDouble = false;
459
+ inRegex = false;
458
460
  }
459
461
  const isEntirelyInsideBlock = isInsideBlockStart && (inBlockJS || inBlockHTML || (commentStartIndex === -1));
460
462
  let isPureComment = false;
@@ -663,6 +665,33 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
663
665
  const filteredText = filtered.map(line => line.text);
664
666
  const filteredIndices = filtered.map(line => line.originalIndex);
665
667
 
668
+ // Validate that all inserted lines (originalIndex === -1) are valid comments or empty lines [ds]
669
+ const insertedLines = annotated.filter(line => line.originalIndex === -1);
670
+ let inInsertedBlock = false;
671
+ for (const item of insertedLines) {
672
+ const trimmed = item.text.trim();
673
+ if (!trimmed) continue;
674
+ if (inInsertedBlock) {
675
+ if (trimmed.includes('*/') || trimmed.includes('-->')) {
676
+ inInsertedBlock = false;
677
+ }
678
+ continue;
679
+ }
680
+ const isValidComment =
681
+ trimmed.startsWith('//') ||
682
+ trimmed.startsWith('/*') ||
683
+ trimmed.startsWith('*') ||
684
+ trimmed.startsWith('#') ||
685
+ trimmed.startsWith('<!--') ||
686
+ trimmed.startsWith('--');
687
+ if (!isValidComment) {
688
+ throw new Error(`Safety Assertion Failed: Refused to insert non-comment code: "${trimmed}"`);
689
+ }
690
+ if ((trimmed.startsWith('/*') && !trimmed.includes('*/')) || (trimmed.startsWith('<!--') && !trimmed.includes('-->'))) {
691
+ inInsertedBlock = true;
692
+ }
693
+ }
694
+
666
695
  const textEqual = filteredText.every((text, idx) => {
667
696
  const origIdx = filteredIndices[idx];
668
697
  const originalLine = originalLines[origIdx];
@@ -710,8 +739,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
710
739
  }
711
740
 
712
741
  if (!textEqual || !indicesSequential) {
713
- console.error("\nSafety Assertion Failed: Spliced code does not match original code minus comments!");
714
- process.exit(1);
742
+ throw new Error("Safety Assertion Failed: Spliced code does not match original code minus comments!");
715
743
  }
716
744
 
717
745
  return annotated.map(line => line.text).join(lineEnding);
@@ -818,7 +846,7 @@ Options:
818
846
  const hasOverwriteFlag = args.includes('--overwrite');
819
847
  const hasKeepFlag = args.includes('--keep');
820
848
 
821
- if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
849
+ if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce && !isDryRun) {
822
850
  console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
823
851
  rl.close();
824
852
  process.exit(1);
@@ -871,9 +899,10 @@ Options:
871
899
 
872
900
  function isPathIgnored(targetPath) {
873
901
  const filename = path.basename(targetPath);
902
+ const relPath = path.relative(process.cwd(), targetPath).replace(/\\/g, '/');
874
903
  for (const pattern of allIgnored) {
875
904
  const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
876
- if (filename === cleanPattern) return true;
905
+ if (filename === cleanPattern || relPath === cleanPattern || relPath.startsWith(cleanPattern + '/')) return true;
877
906
  if (pattern.startsWith('*') && filename.endsWith(pattern.slice(1))) return true;
878
907
  }
879
908
  return false;
@@ -899,7 +928,7 @@ Options:
899
928
  const validExtensions = [
900
929
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
901
930
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
902
- '.swift', '.kt', '.dart', '.sh'
931
+ '.swift', '.kt', '.dart', '.sh', '.sql'
903
932
  ];
904
933
 
905
934
  if (!validExtensions.includes(ext)) {
@@ -1,4 +1,4 @@
1
- const { execSync } = require('child_process');
1
+ const { execSync, execFileSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { spliceComments } = require('./cli');
@@ -30,7 +30,7 @@ try {
30
30
  }
31
31
 
32
32
  // Get a list of changed files in the last commit [ds]
33
- const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
33
+ const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r --root HEAD', { encoding: 'utf8' }).trim();
34
34
  if (!changedFilesStr) {
35
35
  process.exit(0);
36
36
  }
@@ -40,7 +40,7 @@ try {
40
40
  const validExtensions = [
41
41
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
42
42
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
43
- '.swift', '.kt', '.dart', '.sh'
43
+ '.swift', '.kt', '.dart', '.sh', '.sql'
44
44
  ];
45
45
 
46
46
  /** Filter function to determine which files to auto-comment [ds] */
@@ -97,13 +97,13 @@ try {
97
97
  // Log and attempt to auto-comment the current file [ds]
98
98
  console.log(`[devsplain] Automatically commenting file: ${file}`);
99
99
  try {
100
- let extraFlags = '';
101
- if (process.env.DS_OVER) extraFlags += ' --overwrite';
102
- if (process.env.DS_KEEP) extraFlags += ' --keep';
103
-
104
- // Construct the command to run the auto-commenting CLI [ds]
105
100
  const cliPath = path.join(__dirname, 'cli.js');
106
- execSync(`node "${cliPath}" "${file}" --force${modeFlag}${extraFlags}`, { stdio: 'inherit' });
101
+ const cliArgs = [cliPath, file, '--force'];
102
+ if (modeFlag.trim()) cliArgs.push(modeFlag.trim());
103
+ if (process.env.DS_OVER) cliArgs.push('--overwrite');
104
+ if (process.env.DS_KEEP) cliArgs.push('--keep');
105
+
106
+ execFileSync(process.execPath, cliArgs, { stdio: 'inherit' });
107
107
  commentedAny = true;
108
108
  successfullyCommentedFiles.push(file);
109
109
  } catch (err) {
@@ -115,7 +115,7 @@ try {
115
115
  if (commentedAny) {
116
116
  for (const file of successfullyCommentedFiles) {
117
117
  try {
118
- execSync(`git add "${file}"`);
118
+ execFileSync('git', ['add', file]);
119
119
  } catch (addErr) {}
120
120
  }
121
121
 
package/bin/setup-hook.js CHANGED
@@ -57,8 +57,17 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
57
57
  npm test || exit 1
58
58
  fi
59
59
  `;
60
- // Write pre-commit hook to file [ds]
61
- fs.writeFileSync(preCommitHookPath, preCommitContent);
60
+ // Write pre-commit hook safely [ds]
61
+ if (fs.existsSync(preCommitHookPath)) {
62
+ const existing = fs.readFileSync(preCommitHookPath, 'utf8');
63
+ if (!existing.includes('# devsplain native pre-commit hook')) {
64
+ fs.appendFileSync(preCommitHookPath, '\n' + preCommitContent);
65
+ } else {
66
+ fs.writeFileSync(preCommitHookPath, preCommitContent);
67
+ }
68
+ } else {
69
+ fs.writeFileSync(preCommitHookPath, preCommitContent);
70
+ }
62
71
  try {
63
72
  fs.chmodSync(preCommitHookPath, 0o755);
64
73
  } catch (err) {}
@@ -73,8 +82,17 @@ fi
73
82
  echo "Auto-generating comments for files in the last commit..."
74
83
  node "${postCommitScript}"${modeArgs} || exit 1
75
84
  `;
76
- // Write post-commit hook to file [ds]
77
- fs.writeFileSync(postCommitHookPath, postCommitContent);
85
+ // Write post-commit hook safely [ds]
86
+ if (fs.existsSync(postCommitHookPath)) {
87
+ const existing = fs.readFileSync(postCommitHookPath, 'utf8');
88
+ if (!existing.includes('# devsplain native post-commit hook')) {
89
+ fs.appendFileSync(postCommitHookPath, '\n' + postCommitContent);
90
+ } else {
91
+ fs.writeFileSync(postCommitHookPath, postCommitContent);
92
+ }
93
+ } else {
94
+ fs.writeFileSync(postCommitHookPath, postCommitContent);
95
+ }
78
96
  try {
79
97
  fs.chmodSync(postCommitHookPath, 0o755);
80
98
  } catch (err) {}
package/lib/config.js CHANGED
@@ -22,8 +22,14 @@ function askSecret(query) {
22
22
  process.stdout.write('\n');
23
23
  rl.close();
24
24
  resolve(key);
25
+ } else if (char === '\u0008' || char === '\u007f') { // Backspace
26
+ if (key.length > 0) {
27
+ key = key.slice(0, -1);
28
+ process.stdout.write('\b \b');
29
+ }
25
30
  } else if (char === '\u0003') { // Ctrl+C
26
- process.exit();
31
+ process.stdin.setRawMode(false);
32
+ process.exit(130);
27
33
  } else {
28
34
  key += char;
29
35
  process.stdout.write('*');
@@ -35,7 +41,7 @@ function askSecret(query) {
35
41
 
36
42
  // Migrates old config to the new format [ds]
37
43
  function migrateConfig(oldConfig) {
38
- if (oldConfig.provider && !oldConfig.providers) {
44
+ if (oldConfig && typeof oldConfig === 'object' && oldConfig.provider && !oldConfig.providers) {
39
45
  return {
40
46
  activeProvider: oldConfig.provider,
41
47
  providers: {
@@ -75,7 +81,7 @@ async function getConfig(forceWizard = false) {
75
81
  }
76
82
  }
77
83
 
78
- if (!fileConfig || forceWizard) {
84
+ if (!fileConfig || !fileConfig.activeProvider || !fileConfig.providers || !fileConfig.providers[fileConfig.activeProvider] || forceWizard) {
79
85
  let rl = readline.createInterface({
80
86
  input: process.stdin,
81
87
  output: process.stdout
@@ -274,9 +280,10 @@ async function getConfig(forceWizard = false) {
274
280
  ...config.providers[config.activeProvider]
275
281
  };
276
282
  } else {
283
+ const active = fileConfig.providers[fileConfig.activeProvider] || {};
277
284
  return {
278
285
  provider: fileConfig.activeProvider,
279
- ...fileConfig.providers[fileConfig.activeProvider]
286
+ ...active
280
287
  };
281
288
  }
282
289
  }
package/lib/llm.js CHANGED
@@ -4,13 +4,16 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
4
4
  // Loop through each attempt, up to the maximum number of retries [ds]
5
5
  for (let attempt = 0; attempt < maxRetries; attempt++) {
6
6
  const controller = new AbortController();
7
- const timeoutId = setTimeout(() => controller.abort(), 15000);
7
+ const timeoutId = setTimeout(() => controller.abort(), 45000);
8
8
  try {
9
9
  const response = await fetch(url, {
10
10
  ...options,
11
11
  signal: controller.signal
12
12
  });
13
13
  clearTimeout(timeoutId);
14
+ if (!response) {
15
+ throw new Error("No response received from fetch");
16
+ }
14
17
  if (response.ok) {
15
18
  return response;
16
19
  }
@@ -22,7 +25,7 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
22
25
  } catch (err) {
23
26
  clearTimeout(timeoutId);
24
27
  if (err.name === 'AbortError') {
25
- lastError = new Error("Request timed out after 15 seconds");
28
+ lastError = new Error("Request timed out after 45 seconds");
26
29
  } else {
27
30
  lastError = err;
28
31
  }
@@ -46,8 +49,9 @@ async function getComments(code, language, config, mode = 'default') {
46
49
  const extMatch = language.match(/\.[0-9a-z]+$/i);
47
50
  const ext = extMatch ? extMatch[0].toLowerCase() : '';
48
51
  const isPython = ext === '.py';
49
- const isRubyOrShell = ['.rb', '.sh', '.php'].includes(ext);
52
+ const isRubyOrShell = ['.rb', '.sh'].includes(ext);
50
53
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
54
+ const isCss = ['.css', '.scss'].includes(ext);
51
55
  const isSql = ext === '.sql';
52
56
 
53
57
  // Define the single-line comment token and examples [ds]
@@ -63,6 +67,10 @@ async function getComments(code, language, config, mode = 'default') {
63
67
  singleLineToken = '<!--';
64
68
  blockExample = '<!-- Calculates the total price -->';
65
69
  inlineExample = '<!-- Check for null values -->';
70
+ } else if (isCss) {
71
+ singleLineToken = '/*';
72
+ blockExample = '/* Calculates the total price */';
73
+ inlineExample = '/* Check for null values */';
66
74
  } else if (isSql) {
67
75
  singleLineToken = '--';
68
76
  blockExample = '-- Calculates the total price';
@@ -77,6 +85,11 @@ async function getComments(code, language, config, mode = 'default') {
77
85
  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
86
  }
79
87
 
88
+ let rule5 = `5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.`;
89
+ if (isCss) {
90
+ rule5 = `5. IMPORTANT: In CSS/SCSS, you MUST use /* ... */ for comments. DO NOT use // comments under any circumstances.`;
91
+ }
92
+
80
93
  let prompt = `
81
94
  You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
82
95
  ${instruction}
@@ -86,7 +99,7 @@ CRITICAL RULES:
86
99
  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).
87
100
  3. Do NOT include the original code in your response.
88
101
  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.
102
+ ${rule5}
90
103
 
91
104
  Example Output:
92
105
  [
@@ -116,10 +129,15 @@ ${numberedCode}
116
129
  });
117
130
  data = await response.json();
118
131
  } catch (error) {
119
- throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
132
+ throw new Error(`AI Provider Request Failed: ${error.message}`);
120
133
  }
121
134
  if (data.error) {
122
- throw new Error(`API Error: ${data.error.message}`);
135
+ const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
136
+ throw new Error(`API Error: ${msg}`);
137
+ }
138
+ if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts[0]) {
139
+ const reason = data.candidates?.[0]?.finishReason || 'Unknown error';
140
+ throw new Error(`AI Provider returned no content (finish reason: ${reason})`);
123
141
  }
124
142
  textResponse = data.candidates[0].content.parts[0].text;
125
143
  } else if (config.provider === 'claude') {
@@ -145,10 +163,14 @@ ${numberedCode}
145
163
  });
146
164
  data = await response.json();
147
165
  } catch (error) {
148
- throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
166
+ throw new Error(`AI Provider Request Failed: ${error.message}`);
149
167
  }
150
168
  if (data.error) {
151
- throw new Error(`API Error: ${data.error.message}`);
169
+ const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
170
+ throw new Error(`API Error: ${msg}`);
171
+ }
172
+ if (!data.content || !data.content[0] || typeof data.content[0].text !== 'string') {
173
+ throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
152
174
  }
153
175
  textResponse = data.content[0].text;
154
176
  }
@@ -166,6 +188,7 @@ ${numberedCode}
166
188
  },
167
189
  body: JSON.stringify({
168
190
  "model": config.model,
191
+ "max_tokens": 8192,
169
192
  "messages": [{
170
193
  "role": "user",
171
194
  "content": prompt
@@ -174,10 +197,14 @@ ${numberedCode}
174
197
  });
175
198
  data = await response.json();
176
199
  } catch (error) {
177
- throw new Error("Network Error: Could not connect to the AI provider. Check your internet or API url.");
200
+ throw new Error(`AI Provider Request Failed: ${error.message}`);
178
201
  }
179
202
  if (data.error) {
180
- throw new Error(`API Error: ${data.error.message}`);
203
+ const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
204
+ throw new Error(`API Error: ${msg}`);
205
+ }
206
+ if (!data.choices || !data.choices[0] || !data.choices[0].message || typeof data.choices[0].message.content !== 'string') {
207
+ throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
181
208
  }
182
209
  textResponse = data.choices[0].message.content;
183
210
  }
@@ -220,15 +247,30 @@ ${numberedCode}
220
247
  }
221
248
 
222
249
  const trimmedComment = item.comment.trim();
223
- const startsWithCommentMarker =
224
- trimmedComment.startsWith('//') ||
225
- trimmedComment.startsWith('/*') ||
226
- trimmedComment.startsWith('#') ||
227
- trimmedComment.startsWith('<!--') ||
228
- trimmedComment.startsWith('--');
229
-
230
- if (!startsWithCommentMarker) {
231
- throw new Error(`Security Error: Comment on line ${item.line} does not start with a valid comment character sequence. Rejected: ${trimmedComment}`);
250
+ const commentLines = trimmedComment.split(/\r?\n/);
251
+ let inBlock = false;
252
+ for (const cl of commentLines) {
253
+ const tcl = cl.trim();
254
+ if (!tcl) continue;
255
+ if (inBlock) {
256
+ if (tcl.includes('*/') || tcl.includes('-->')) {
257
+ inBlock = false;
258
+ }
259
+ continue;
260
+ }
261
+ const startsWithMarker =
262
+ tcl.startsWith('//') ||
263
+ tcl.startsWith('/*') ||
264
+ tcl.startsWith('*') ||
265
+ tcl.startsWith('#') ||
266
+ tcl.startsWith('<!--') ||
267
+ tcl.startsWith('--');
268
+ if (!startsWithMarker) {
269
+ throw new Error(`Security Error: Comment on line ${item.line} contains invalid non-comment line: "${tcl}"`);
270
+ }
271
+ if ((tcl.startsWith('/*') && !tcl.includes('*/')) || (tcl.startsWith('<!--') && !tcl.includes('-->'))) {
272
+ inBlock = true;
273
+ }
232
274
  }
233
275
  }
234
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
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",