devsplain 2.2.1 → 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
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
10
10
  let rl;
11
11
  let askQuestion;
12
12
 
13
- // Determines whether the current Git repository has uncommitted changes [ds]
13
+ // Checks if the Git repository 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 whether a given line of code is inside a string literal [ds]
26
+ // Determines if a line 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());
@@ -231,12 +231,13 @@ 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;
237
238
  }
238
239
 
239
- // Analyzes comments within a given list of code lines [ds]
240
+ // Analyzes comments in the given lines of code [ds]
240
241
  function analyzeComments(lines, ext = '') {
241
242
  const isPython = ext.toLowerCase() === '.py';
242
243
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -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;
@@ -477,7 +479,7 @@ function analyzeComments(lines, ext = '') {
477
479
  return analysis;
478
480
  }
479
481
 
480
- // Inserts or removes comments from code based on the specified mode [ds]
482
+ // Splices comments into the provided data [ds]
481
483
  function spliceComments(data, comments, mode = 'default', ext = '') {
482
484
  const hasCRLF = data.includes('\r\n');
483
485
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -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,14 +739,13 @@ 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);
718
746
  }
719
747
 
720
- // Runs the command-line interface for the application [ds]
748
+ // Runs the CLI interface for the application [ds]
721
749
  async function runCLI() {
722
750
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
723
751
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -744,6 +772,7 @@ Options:
744
772
  --base-url <url> Override base URL for custom APIs
745
773
  --config Force run the configuration setup wizard
746
774
  --setup-hook Install Git pre-commit and post-commit hooks in repository
775
+ --remove-hook Remove devsplain-installed Git hooks from repository
747
776
  --help, -h Show this help message
748
777
  --version, -v Show version information
749
778
  `);
@@ -767,11 +796,18 @@ Options:
767
796
 
768
797
  if (args.includes('--setup-hook')) {
769
798
  rl.close();
770
- const installHooks = require('./setup-hook.js');
799
+ const { installHooks } = require('./setup-hook.js');
771
800
  await installHooks();
772
801
  return;
773
802
  }
774
803
 
804
+ if (args.includes('--remove-hook')) {
805
+ rl.close();
806
+ const { removeHooks } = require('./setup-hook.js');
807
+ await removeHooks();
808
+ return;
809
+ }
810
+
775
811
  const getArgValue = (flag) => {
776
812
  const index = args.indexOf(flag);
777
813
  if (index !== -1 && index + 1 < args.length) {
@@ -810,7 +846,7 @@ Options:
810
846
  const hasOverwriteFlag = args.includes('--overwrite');
811
847
  const hasKeepFlag = args.includes('--keep');
812
848
 
813
- if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
849
+ if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce && !isDryRun) {
814
850
  console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
815
851
  rl.close();
816
852
  process.exit(1);
@@ -863,9 +899,10 @@ Options:
863
899
 
864
900
  function isPathIgnored(targetPath) {
865
901
  const filename = path.basename(targetPath);
902
+ const relPath = path.relative(process.cwd(), targetPath).replace(/\\/g, '/');
866
903
  for (const pattern of allIgnored) {
867
904
  const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
868
- if (filename === cleanPattern) return true;
905
+ if (filename === cleanPattern || relPath === cleanPattern || relPath.startsWith(cleanPattern + '/')) return true;
869
906
  if (pattern.startsWith('*') && filename.endsWith(pattern.slice(1))) return true;
870
907
  }
871
908
  return false;
@@ -891,7 +928,7 @@ Options:
891
928
  const validExtensions = [
892
929
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
893
930
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
894
- '.swift', '.kt', '.dart', '.sh'
931
+ '.swift', '.kt', '.dart', '.sh', '.sql'
895
932
  ];
896
933
 
897
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
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const readline = require('readline');
5
5
 
6
- /** Installs Git hooks and configures commit settings [ds] */
6
+ // Installs Git hooks for automatic comment generation [ds]
7
7
  async function installHooks() {
8
8
  try {
9
9
  const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
@@ -14,7 +14,7 @@ async function installHooks() {
14
14
  }
15
15
 
16
16
  let modeChoice = '1';
17
- // Check if process is run in a TTY environment [ds]
17
+ // Prompt user for commenting mode if running in a TTY [ds]
18
18
  if (process.stdout.isTTY) {
19
19
  const rl = readline.createInterface({
20
20
  input: process.stdin,
@@ -22,13 +22,13 @@ async function installHooks() {
22
22
  });
23
23
  const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
24
24
 
25
- // Prompt user to select the default commenting mode [ds]
25
+ // Display commenting mode options to the user [ds]
26
26
  console.log('\nSelect default commenting mode for Git commits:');
27
27
  console.log('1. Balanced (mix of JSDoc and sparse inline comments)');
28
28
  console.log('2. Light (JSDoc block comments above functions only)');
29
29
  console.log('3. Full (aggressive inline commenting)');
30
30
 
31
- // Validate user input for commenting mode [ds]
31
+ // Loop until a valid mode choice is selected [ds]
32
32
  while (true) {
33
33
  const answer = (await askQuestion('Select (1-3, default: 1): ')).trim();
34
34
  if (answer === '' || ['1', '2', '3'].includes(answer)) {
@@ -40,7 +40,7 @@ async function installHooks() {
40
40
  rl.close();
41
41
  }
42
42
 
43
- // Determine mode arguments based on user choice [ds]
43
+ // Determine mode arguments based on user selection [ds]
44
44
  let modeArgs = '';
45
45
  if (modeChoice === '2') {
46
46
  modeArgs = ' --light';
@@ -48,7 +48,7 @@ async function installHooks() {
48
48
  modeArgs = ' --full';
49
49
  }
50
50
 
51
- // Define the pre-commit hook script [ds]
51
+ // Create pre-commit hook script [ds]
52
52
  const preCommitHookPath = path.join(hooksDir, 'pre-commit');
53
53
  const preCommitContent = `#!/bin/sh
54
54
  # devsplain native pre-commit hook
@@ -57,63 +57,144 @@ 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 the pre-commit hook to the Git hooks directory [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) {}
65
74
 
66
- // Define the post-commit script path [ds]
75
+ // Path to post-commit script [ds]
67
76
  const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
68
77
 
69
- // Define the post-commit hook script [ds]
78
+ // Create post-commit hook script [ds]
70
79
  const postCommitHookPath = path.join(hooksDir, 'post-commit');
71
80
  const postCommitContent = `#!/bin/sh
72
81
  # devsplain native post-commit hook
73
82
  echo "Auto-generating comments for files in the last commit..."
74
83
  node "${postCommitScript}"${modeArgs} || exit 1
75
84
  `;
76
- 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
+ }
77
96
  try {
78
97
  fs.chmodSync(postCommitHookPath, 0o755);
79
98
  } catch (err) {}
80
99
 
81
- // Inform the user about the successful installation of the post-commit hook [ds]
100
+ // Log successful installation of post-commit hook [ds]
82
101
  console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
83
102
 
84
- // Check if a .devsplainignore file exists in the Git root directory [ds]
103
+ // Path to devsplain ignore file [ds]
85
104
  const ignorePath = path.join(gitRoot, '.devsplainignore');
105
+ const defaultIgnoreLines = [
106
+ 'node_modules/', '.git/', 'dist/', 'build/', 'out/',
107
+ '.next/', '.nuxt/', '.svelte-kit/',
108
+ 'venv/', 'env/', '.venv/',
109
+ '.vscode/', '.idea/', 'coverage/',
110
+ 'tests/', '__tests__/', 'fixtures/'
111
+ ];
112
+ // List of default patterns to ignore [ds]
113
+
114
+ const gitignorePath = path.join(gitRoot, '.gitignore');
115
+ let gitignoreLines = [];
116
+ if (fs.existsSync(gitignorePath)) {
117
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
118
+ gitignoreLines = gitignoreContent.split(/\r?\n/)
119
+ .map(l => l.trim())
120
+ .filter(l => l && !l.startsWith('#'));
121
+ }
122
+
123
+ // Check if .gitignore file exists [ds]
86
124
  if (!fs.existsSync(ignorePath)) {
87
- const defaultIgnore = `node_modules/
88
- .git/
89
- dist/
90
- build/
91
- out/
92
- .next/
93
- .nuxt/
94
- .svelte-kit/
95
- venv/
96
- env/
97
- .venv/
98
- .vscode/
99
- .idea/
100
- coverage/
101
- tests/
102
- __tests__/
103
- fixtures/
104
- `;
105
- fs.writeFileSync(ignorePath, defaultIgnore);
106
- console.log(`[devsplain] Created default .devsplainignore file at: ${ignorePath}`);
125
+ const gitignoreOnly = gitignoreLines.filter(p => !defaultIgnoreLines.includes(p));
126
+ let content = defaultIgnoreLines.join('\n') + '\n';
127
+ if (gitignoreOnly.length > 0) {
128
+ content += '\n# From .gitignore\n' + gitignoreOnly.join('\n') + '\n';
129
+ }
130
+ fs.writeFileSync(ignorePath, content);
131
+ console.log(`[devsplain] Created .devsplainignore at: ${ignorePath}`);
132
+ if (gitignoreOnly.length > 0) {
133
+ console.log(`[devsplain] Merged ${gitignoreOnly.length} pattern(s) from .gitignore into .devsplainignore.`);
134
+ }
135
+ } else {
136
+ const existingContent = fs.readFileSync(ignorePath, 'utf8');
137
+ const existingLines = existingContent.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
138
+ const newPatterns = gitignoreLines.filter(p => !existingLines.includes(p));
139
+ if (newPatterns.length > 0) {
140
+ const appendContent = '\n# From .gitignore\n' + newPatterns.join('\n') + '\n';
141
+ fs.appendFileSync(ignorePath, appendContent);
142
+ console.log(`[devsplain] Merged ${newPatterns.length} new pattern(s) from .gitignore into .devsplainignore.`);
143
+ } else if (gitignoreLines.length > 0) {
144
+ console.log('[devsplain] .devsplainignore is already up-to-date with .gitignore patterns.');
145
+ }
146
+ // Handle any errors during hook installation [ds]
107
147
  }
148
+
108
149
  } catch (e) {
109
150
  console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
110
151
  console.warn(e.message);
111
152
  }
112
153
  }
113
154
 
114
- // Check if the script is run as the main module [ds]
155
+ // Removes Git hooks installed by devsplain [ds]
156
+ async function removeHooks() {
157
+ try {
158
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
159
+ const hooksDir = path.join(gitDir, 'hooks');
160
+ const hookSignatures = {
161
+ 'pre-commit': '# devsplain native pre-commit hook',
162
+ 'post-commit': '# devsplain native post-commit hook'
163
+ };
164
+
165
+ let removed = 0;
166
+ // Iterate through hook signatures to remove [ds]
167
+ for (const [hookName, signature] of Object.entries(hookSignatures)) {
168
+ const hookPath = path.join(hooksDir, hookName);
169
+ if (fs.existsSync(hookPath)) {
170
+ const content = fs.readFileSync(hookPath, 'utf8');
171
+ if (content.includes(signature)) {
172
+ fs.unlinkSync(hookPath);
173
+ console.log(`[devsplain] Removed ${hookName} hook at: ${hookPath}`);
174
+ removed++;
175
+ } else {
176
+ console.log(`[devsplain] Skipping ${hookName}: not installed by devsplain.`);
177
+ }
178
+ } else {
179
+ console.log(`[devsplain] No ${hookName} hook found at: ${hookPath}`);
180
+ }
181
+ }
182
+
183
+ // Log removal of hooks [ds]
184
+ if (removed > 0) {
185
+ console.log(`[devsplain] Successfully removed ${removed} hook(s).`);
186
+ } else {
187
+ console.log('[devsplain] No devsplain hooks were found to remove.');
188
+ }
189
+ } catch (e) {
190
+ console.warn('Warning: Could not remove Git hooks (not inside a git repository or git command missing).');
191
+ console.warn(e.message);
192
+ }
193
+ }
194
+
195
+ // Run installHooks if this script is executed directly [ds]
115
196
  if (require.main === module) {
116
197
  installHooks();
117
198
  }
118
- // Export the installHooks function for external use [ds]
119
- module.exports = installHooks;
199
+ // Export installHooks and removeHooks functions [ds]
200
+ module.exports = { installHooks, removeHooks };
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.1",
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",