devsplain 1.6.0 → 1.7.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/README.md CHANGED
@@ -6,7 +6,7 @@ An industrial-grade, agent-agnostic CLI tool that automatically adds JSDoc and i
6
6
 
7
7
  ## Key Features
8
8
 
9
- - **Mathematical Safety Invariants**: Uses an index-preserving splicing engine. Your functional code is mathematically verified to remain identical before and after commenting.
9
+ - **Deterministic Code Integrity Verification**: Uses an index-preserving splicing engine. Your non-comment source lines are guaranteed to remain byte-for-byte identical after comment insertion.
10
10
  - **Multi-Language support**: Natively parses JavaScript, JSX, TypeScript, TSX, HTML, CSS, SCSS, Vue, Svelte, Python, Java, C, C++, C#, Go, Ruby, PHP, Rust, Swift, Kotlin, Dart, and Shell scripts.
11
11
  - **Comment Preservation & Tagging**: AI-generated comments are tagged with `[ds]`. Your manually written comments are safe and will never be touched by the engine.
12
12
  - **Local Deterministic Scrubber**: The `--clean` flag strips AI-generated `[ds]` comments locally using a deterministic lexical state machine—no LLM calls, API keys, or internet required.
@@ -35,6 +35,19 @@ Many AI code formatters rewrite your code entirely, exposing you to logic regres
35
35
  ### String Literal Guardrails
36
36
  The engine tracks lexical state across template strings, single quotes, double quotes, and multi-line docstrings (such as Python triple-quotes). Comment insertion is blocked if the target line resides within a string literal, preventing broken syntax.
37
37
 
38
+ ### Why Not AST Verification?
39
+
40
+ AST verification would require language-specific parser dependencies for every supported language.
41
+
42
+ `devsplain` instead uses deterministic source-preservation verification:
43
+
44
+ 1. Original source is loaded.
45
+ 2. Comments are inserted.
46
+ 3. Generated comments are removed.
47
+ 4. The remaining source must match the original file exactly.
48
+
49
+ If any non-comment source line differs, the operation aborts.
50
+
38
51
  ---
39
52
 
40
53
  ## Installation
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 current Git repository is dirty by inspecting status. */
13
+ /** Checks if the current 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,8 +23,7 @@ function isGitDirty() {
23
23
  return false;
24
24
  }
25
25
 
26
- /** Determines if a specific line index is within a string literal (handling quotes/backticks). [ds] */
27
- /** Determines if a specific line index is within a string literal (handling quotes/backticks). */
26
+ /** Checks if a given line index is inside a string in a list of code lines [ds] */
28
27
  function isLineInsideString(lines, targetLineIndex, ext = '') {
29
28
  const isPython = ext.toLowerCase() === '.py';
30
29
  let inBacktick = false;
@@ -98,8 +97,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
98
97
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
99
98
  }
100
99
 
101
- /** Parses a file to identify pure comments and block structures. [ds] */
102
- /** Parses a file to identify pure comments and block structures. */
100
+ /** Analyzes comments in a list of code lines [ds] */
103
101
  function analyzeComments(lines, ext = '') {
104
102
  const isPython = ext.toLowerCase() === '.py';
105
103
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -135,12 +133,23 @@ function analyzeComments(lines, ext = '') {
135
133
  j++;
136
134
  continue;
137
135
  }
136
+ // Check for comment start index in non-Python files [ds]
138
137
  if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
139
138
  if (isPython) {
140
139
  if (line[j] === '#') {
141
140
  commentStartIndex = j;
142
141
  break;
143
142
  }
143
+ if (line.slice(j, j + 2) === '/*') {
144
+ commentStartIndex = j;
145
+ inBlockJS = true;
146
+ j += 2;
147
+ continue;
148
+ }
149
+ if (line.slice(j, j + 2) === '//') {
150
+ commentStartIndex = j;
151
+ break;
152
+ }
144
153
  } else if (isHTML) {
145
154
  if (line.slice(j, j + 4) === '<!--') {
146
155
  commentStartIndex = j;
@@ -148,6 +157,12 @@ function analyzeComments(lines, ext = '') {
148
157
  j += 4;
149
158
  continue;
150
159
  }
160
+ if (line.slice(j, j + 2) === '/*') {
161
+ commentStartIndex = j;
162
+ inBlockJS = true;
163
+ j += 2;
164
+ continue;
165
+ }
151
166
  if (line.slice(j, j + 2) === '//') {
152
167
  commentStartIndex = j;
153
168
  break;
@@ -184,6 +199,7 @@ function analyzeComments(lines, ext = '') {
184
199
  continue;
185
200
  }
186
201
  }
202
+ // Check for string literals in non-Python files [ds]
187
203
  } else {
188
204
  if (!inSingle && !inDouble) {
189
205
  if (line[j] === '`') {
@@ -223,6 +239,7 @@ function analyzeComments(lines, ext = '') {
223
239
  }
224
240
  }
225
241
  }
242
+ // Increment character index [ds]
226
243
  j++;
227
244
  }
228
245
  if (!isPython) {
@@ -250,8 +267,7 @@ function analyzeComments(lines, ext = '') {
250
267
  return analysis;
251
268
  }
252
269
 
253
- /** Splices comments into code or cleans existing ones, with safety checks. [ds] */
254
- /** Splices comments into code or cleans existing ones, with safety checks. */
270
+ /** Splices comments into a list of code lines [ds] */
255
271
  function spliceComments(data, comments, mode = 'default', ext = '') {
256
272
  const hasCRLF = data.includes('\r\n');
257
273
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -266,7 +282,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
266
282
  if (mode === 'clean' || mode === 'prune') {
267
283
  analysis = analyzeComments(originalLines, ext);
268
284
  const finalDeletions = new Set();
269
-
270
285
  if (mode === 'clean') {
271
286
  let i = 0;
272
287
  while (i < originalLines.length) {
@@ -274,15 +289,12 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
274
289
  let start = i;
275
290
  let end = i;
276
291
  while (end < originalLines.length && analysis[end].isInsideBlock) end++;
277
-
278
292
  let blockStart = start - 1;
279
293
  let blockEnd = end - 1;
280
-
281
294
  let hasDs = false;
282
295
  for (let k = blockStart; k <= blockEnd; k++) {
283
296
  if (originalLines[k].includes('[ds]')) hasDs = true;
284
297
  }
285
-
286
298
  if (hasDs) {
287
299
  for (let k = blockStart; k <= blockEnd; k++) {
288
300
  dsBlocks.add(k + 1);
@@ -420,7 +432,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
420
432
  if (lineAnalysis && lineAnalysis.commentStartIndex !== -1 && !lineAnalysis.isPureComment) {
421
433
  const isDsBlockLine = dsBlocks.has(origIdx + 1);
422
434
  const hasDsInline = originalLine.includes('[ds]');
423
-
424
435
  if (mode === 'prune' || (mode === 'clean' && (hasDsInline || isDsBlockLine))) {
425
436
  const expectedStripped = originalLine.slice(0, lineAnalysis.commentStartIndex).trimEnd();
426
437
  if (text === expectedStripped) {
@@ -448,8 +459,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
448
459
  return annotated.map(line => line.text).join(lineEnding);
449
460
  }
450
461
 
451
- /** Main entry point for the CLI tool. [ds] */
452
- /** Main entry point for the CLI tool. */
462
+ /** Runs the CLI interface for the commenting tool [ds] */
453
463
  async function runCLI() {
454
464
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
455
465
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -504,7 +514,6 @@ Options:
504
514
  return;
505
515
  }
506
516
 
507
- // Helper to extract flag values from command line arguments [ds]
508
517
  const getArgValue = (flag) => {
509
518
  const index = args.indexOf(flag);
510
519
  if (index !== -1 && index + 1 < args.length) {
@@ -570,9 +579,8 @@ Options:
570
579
  let successCount = 0;
571
580
  let failCount = 0;
572
581
 
573
- /** Recursively processes files or directories to apply AI-generated comments. [ds] */
574
- /** Recursively processes files or directories to apply AI-generated comments. */
575
582
  async function processPath(targetPath) {
583
+ // Process a directory or file path [ds]
576
584
  const stats = fs.statSync(targetPath);
577
585
 
578
586
  if (stats.isDirectory()) {
@@ -581,7 +589,8 @@ Options:
581
589
  'node_modules', '.git', 'dist', 'build', 'out',
582
590
  '.next', '.nuxt', '.svelte-kit',
583
591
  'venv', 'env', '.venv',
584
- '.vscode', '.idea', 'coverage'
592
+ '.vscode', '.idea', 'coverage',
593
+ 'tests', '__tests__', 'fixtures'
585
594
  ];
586
595
 
587
596
  if (ignoredFolders.includes(folderName)) {
@@ -618,12 +627,12 @@ Options:
618
627
  try {
619
628
  let comments = [];
620
629
  let commentedCode;
621
- if (mode !== 'clean') {
630
+ if (mode !== 'clean' && mode !== 'prune') {
622
631
  const cleanData = spliceComments(data, [], 'clean', ext);
623
632
  comments = await getComments(cleanData, filename, config, mode);
624
633
  commentedCode = spliceComments(cleanData, comments, mode, ext);
625
634
  } else {
626
- commentedCode = spliceComments(data, [], 'clean', ext);
635
+ commentedCode = spliceComments(data, [], mode, ext);
627
636
  }
628
637
  if (isDryRun) {
629
638
  console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
@@ -668,8 +677,7 @@ Options:
668
677
  rl.close();
669
678
  }
670
679
 
671
- // Check if the script is run directly vs required as a module [ds]
672
- // Check if the script is run directly vs required as a module
680
+ // Run the CLI interface if this script is the main module [ds]
673
681
  if (require.main === module) {
674
682
  runCLI().catch(err => {
675
683
  console.error(err);
@@ -3,45 +3,43 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { spliceComments } = require('./cli');
5
5
 
6
- /** Main execution block to detect changes and process documentation generation [ds] */
7
- /** Main execution block to detect changes and process documentation generation */
6
+ /** Attempts to execute the post-commit hook [ds] */
8
7
  try {
9
- // Prevent recursive loops if the previous commit was an automated documentation commit
10
- // Retrieve the last commit message to prevent infinite loops during automated commits [ds]
8
+ // Get the last commit message [ds]
11
9
  const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
12
10
  if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
13
11
  process.exit(0);
14
12
  }
15
13
 
16
- // Get the list of file paths modified in the current commit [ds]
14
+ // Get the list of changed files in the last commit [ds]
17
15
  const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
18
16
  if (!changedFilesStr) {
19
17
  process.exit(0);
20
18
  }
21
- // Retrieve list of files modified in the latest commit
22
19
  const changedFiles = changedFilesStr.split(/\r?\n/);
23
20
 
24
- // Define allowed file types for documentation processing [ds]
21
+ /** Defines a list of valid file extensions for commenting [ds] */
25
22
  const validExtensions = [
26
23
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
27
24
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
28
25
  '.swift', '.kt', '.dart', '.sh'
29
26
  ];
30
27
 
31
- // Filter for supported file extensions and ensure the file still exists
28
+ /** Filters the changed files based on valid extensions and existence [ds] */
32
29
  const filesToComment = changedFiles.filter(file => {
33
30
  const ext = path.extname(file).toLowerCase();
34
- return validExtensions.includes(ext) && fs.existsSync(file);
31
+ const isIgnored = file.includes('node_modules/') || file.includes('tests/') || file.includes('__tests__/') || file.includes('fixtures/');
32
+ return validExtensions.includes(ext) && fs.existsSync(file) && !isIgnored;
35
33
  });
36
34
 
37
35
  if (filesToComment.length === 0) {
38
36
  process.exit(0);
39
37
  }
40
38
 
39
+ // Log the number of files to be commented [ds]
41
40
  console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
42
41
 
43
- // Parse command line arguments to determine documentation verbosity mode
44
- // Parse CLI arguments for verbosity preferences [ds]
42
+ // Parse command-line arguments for commenting mode [ds]
45
43
  const args = process.argv.slice(2);
46
44
  let modeFlag = '';
47
45
  if (args.includes('--light')) modeFlag = ' --light';
@@ -49,15 +47,13 @@ try {
49
47
 
50
48
  let commentedAny = false;
51
49
 
52
- // Process each valid file to determine if documentation needs updating
50
+ /** Iterates through the files to be commented and attempts to comment each one [ds] */
53
51
  for (const file of filesToComment) {
54
52
  try {
55
53
  const ext = path.extname(file).toLowerCase();
56
54
  const contentHead = fs.readFileSync(file, 'utf8');
57
55
  let contentPrev = '';
58
56
  try {
59
- // Attempt to retrieve the file version from the previous commit for change comparison [ds]
60
- // Attempt to fetch the file content from the previous commit state for comparison
61
57
  contentPrev = execSync(`git show HEAD~1:"${file}"`, {
62
58
  encoding: 'utf8',
63
59
  stdio: ['ignore', 'pipe', 'ignore']
@@ -65,12 +61,10 @@ try {
65
61
  } catch (prevErr) {
66
62
  }
67
63
 
64
+ // Check if the file has been modified beyond just comments [ds]
68
65
  if (contentPrev) {
69
- // Prune comments to isolate actual code changes and ignore documentation-only commits [ds]
70
- // Strip comments from head and previous versions to detect if logic actually changed
71
66
  const cleanHead = spliceComments(contentHead, [], 'prune', ext);
72
67
  const cleanPrev = spliceComments(contentPrev, [], 'prune', ext);
73
- // Skip processing if only comments were modified in the commit
74
68
  if (cleanHead === cleanPrev) {
75
69
  console.log(`[devsplain] Skipping ${file}: commit contains only comment changes.`);
76
70
  continue;
@@ -79,10 +73,9 @@ try {
79
73
  } catch (cleanErr) {
80
74
  }
81
75
 
76
+ // Attempt to comment the file using the cli script [ds]
82
77
  console.log(`[devsplain] Automatically commenting file: ${file}`);
83
78
  try {
84
- // Execute the CLI generator for the specific file
85
- // Path to the underlying documentation generation engine [ds]
86
79
  const cliPath = path.join(__dirname, 'cli.js');
87
80
  execSync(`node "${cliPath}" "${file}" --force${modeFlag}`, { stdio: 'inherit' });
88
81
  commentedAny = true;
@@ -91,8 +84,7 @@ try {
91
84
  }
92
85
  }
93
86
 
94
- // Stage changes back to the repo if new documentation was generated [ds]
95
- // If changes were made by the generator, stage and commit the result back to the repository
87
+ /** If any files were commented, stage and commit the changes [ds] */
96
88
  if (commentedAny) {
97
89
  const status = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
98
90
  if (status.length > 0) {
package/bin/setup-hook.js CHANGED
@@ -3,29 +3,26 @@ const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const readline = require('readline');
5
5
 
6
- /**
7
- * Orchestrates the installation of Git pre-commit and post-commit hooks.
8
- * Sets up necessary directories and writes hook files with user-selected configuration.
9
- */
6
+ /** Installs Git hooks for the repository. [ds] */
10
7
  async function installHooks() {
11
8
  try {
12
- // Determine the actual git directory path using git command line tool
13
9
  const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
14
10
  const hooksDir = path.join(gitDir, 'hooks');
15
- // Ensure the hooks directory exists before attempting to write files
16
11
  if (!fs.existsSync(hooksDir)) {
17
12
  fs.mkdirSync(hooksDir, { recursive: true });
18
13
  }
19
14
 
20
15
  let modeChoice = '1';
21
- // Prompt the user for mode selection only if running in an interactive terminal session
16
+ // Check if process is running in a TTY to prompt for user input [ds]
22
17
  if (process.stdout.isTTY) {
23
18
  const rl = readline.createInterface({
24
19
  input: process.stdin,
25
20
  output: process.stdout
26
21
  });
22
+ // Create a readline interface for user input [ds]
27
23
  const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
28
24
 
25
+ // Display a menu for the user to select the default commenting mode [ds]
29
26
  console.log('\nSelect default commenting mode for Git commits:');
30
27
  console.log('1. Balanced (mix of JSDoc and sparse inline comments)');
31
28
  console.log('2. Light (JSDoc block comments above functions only)');
@@ -35,7 +32,7 @@ async function installHooks() {
35
32
  rl.close();
36
33
  }
37
34
 
38
- // Map user input to CLI arguments for the post-commit script execution
35
+ // Determine the command line arguments based on the chosen mode [ds]
39
36
  let modeArgs = '';
40
37
  if (modeChoice === '2') {
41
38
  modeArgs = ' --light';
@@ -43,7 +40,7 @@ async function installHooks() {
43
40
  modeArgs = ' --full';
44
41
  }
45
42
 
46
- // Generate and write the pre-commit shell script to trigger tests before committing
43
+ // Define the path to the pre-commit hook file [ds]
47
44
  const preCommitHookPath = path.join(hooksDir, 'pre-commit');
48
45
  const preCommitContent = `#!/bin/sh
49
46
  # devsplain native pre-commit hook
@@ -53,15 +50,15 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
53
50
  fi
54
51
  `;
55
52
  fs.writeFileSync(preCommitHookPath, preCommitContent);
53
+ // Attempt to set the execute permissions for the pre-commit hook file [ds]
56
54
  try {
57
- // Apply execute permissions to the hook file
58
55
  fs.chmodSync(preCommitHookPath, 0o755);
59
56
  } catch (err) {}
60
57
 
61
- // Normalize the path for cross-platform compatibility when injecting into shell script
58
+ // Define the path to the post-commit script [ds]
62
59
  const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
63
60
 
64
- // Generate and write the post-commit shell script to execute the documentation generation
61
+ // Define the path to the post-commit hook file [ds]
65
62
  const postCommitHookPath = path.join(hooksDir, 'post-commit');
66
63
  const postCommitContent = `#!/bin/sh
67
64
  # devsplain native post-commit hook
@@ -69,19 +66,22 @@ echo "Auto-generating comments for files in the last commit..."
69
66
  node "${postCommitScript}"${modeArgs} || exit 1
70
67
  `;
71
68
  fs.writeFileSync(postCommitHookPath, postCommitContent);
72
- // Apply execute permissions to the hook file
69
+ // Attempt to set the execute permissions for the post-commit hook file [ds]
73
70
  try {
74
71
  fs.chmodSync(postCommitHookPath, 0o755);
75
72
  } catch (err) {}
76
73
 
74
+ // Display a success message if the hooks are installed successfully [ds]
77
75
  console.log('Success: Native Git pre-commit and post-commit hooks installed successfully!');
76
+ // Catch and handle any exceptions that occur during the installation process [ds]
78
77
  } catch (e) {
79
78
  console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
80
79
  }
81
80
  }
82
81
 
83
- // Execute installation automatically if this script is run as the entry point
82
+ // Check if this script is being run directly (not being required as a module) [ds]
84
83
  if (require.main === module) {
85
84
  installHooks();
86
85
  }
86
+ // Export the installHooks function as a module [ds]
87
87
  module.exports = installHooks;
package/lib/config.js CHANGED
@@ -4,14 +4,8 @@ const os = require('os');
4
4
  const readline = require('readline');
5
5
  const configPath = path.join(os.homedir(), '.devsplainrc');
6
6
 
7
- /**
8
- * Retrieves configuration from environment variables or a local config file.
9
- * Runs a setup wizard if configuration is missing or forced.
10
- * @param {boolean} forceWizard - Whether to force the interactive setup.
11
- * @returns {Promise<Object>} The configuration object.
12
- */
7
+ /** Retrieves the configuration for the AI provider. If a configuration file exists and no forceWizard flag is provided, the existing configuration will be returned. Otherwise, the function will prompt the user to input their configuration details. [ds] */
13
8
  async function getConfig(forceWizard = false) {
14
- // Priority 1: Check environment variables for configuration
15
9
  if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
16
10
  const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
17
11
  const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile');
@@ -24,33 +18,34 @@ async function getConfig(forceWizard = false) {
24
18
  };
25
19
  }
26
20
 
27
- // Priority 2: Check for existing config file or run setup wizard
21
+ // Check if configuration file exists or if forceWizard flag is set [ds]
28
22
  if (!fs.existsSync(configPath) || forceWizard) {
29
23
  const rl = readline.createInterface({
30
24
  input: process.stdin,
31
25
  output: process.stdout
32
26
  });
33
- // Promisify readline to allow async flow
34
27
  const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
35
28
 
36
29
  let config = null;
37
30
  let confirmed = false;
38
31
 
39
- // Loop until user confirms configuration choices
32
+ // Continuously prompt user for configuration details until confirmation [ds]
40
33
  while (!confirmed) {
41
34
  let baseUrl = "";
42
35
  let model = "";
43
36
  let provider = "";
44
37
 
38
+ // Display available AI provider options [ds]
45
39
  console.log("\nWhich AI Provider Do You want to use?");
46
40
  console.log("1. Groq (Free, Fast, Llama-3)");
47
41
  console.log("2. Gemini (Free Tier)");
48
42
  console.log("3. OpenAI (Paid)");
49
43
  console.log("4. Custom (Ollama, local, etc)");
50
44
 
45
+ // Get user's selected AI provider option [ds]
51
46
  const choice = await askQuestion("Select (1-4): ");
52
47
 
53
- // Handle specific provider setup logic
48
+ // Handle selected AI provider option [ds]
54
49
  if (choice === '1') {
55
50
  provider = 'groq';
56
51
  baseUrl = 'https://api.groq.com/openai';
@@ -78,16 +73,18 @@ async function getConfig(forceWizard = false) {
78
73
  continue;
79
74
  }
80
75
 
76
+ // Get API key from user [ds]
81
77
  const apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
82
78
 
79
+ // Display configuration summary [ds]
83
80
  console.log("\n--- Configuration Summary ---");
84
81
  console.log(`Provider: ${provider}`);
85
82
  console.log(`Model: ${model}`);
86
83
  console.log(`Base URL: ${baseUrl || 'N/A'}`);
87
- // Mask API key for security in display
88
84
  console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
89
85
  console.log("-----------------------------\n");
90
86
 
87
+ // Confirm configuration with user [ds]
91
88
  const confirm = await askQuestion("Does this look correct? (y/n, default: y): ");
92
89
  if (confirm.toLowerCase() === 'y' || confirm.trim() === '') {
93
90
  config = {
@@ -104,10 +101,10 @@ async function getConfig(forceWizard = false) {
104
101
 
105
102
  rl.close();
106
103
 
107
- // Save finalized config to home directory
104
+ // Write configuration to file [ds]
108
105
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
109
106
  try {
110
- // Ensure config file is not readable by other users (POSIX only)
107
+ // Set file permissions to prevent unauthorized access [ds]
111
108
  if (process.platform !== 'win32') {
112
109
  fs.chmodSync(configPath, 0o600);
113
110
  }
@@ -116,10 +113,11 @@ async function getConfig(forceWizard = false) {
116
113
 
117
114
  return config;
118
115
  } else {
119
- // Priority 3: Load from existing file
116
+ // Read existing configuration from file [ds]
120
117
  const rawData = fs.readFileSync(configPath, 'utf8');
121
118
  return JSON.parse(rawData);
122
119
  }
123
120
  }
124
121
 
122
+ // Export the getConfig function [ds]
125
123
  module.exports = { getConfig };
package/lib/llm.js CHANGED
@@ -1,14 +1,7 @@
1
- /**
2
- * Fetches a URL with retry logic, timeout handling, and exponential backoff.
3
- * @param {string} url - The URL to fetch.
4
- * @param {object} options - Fetch options.
5
- * @param {number} maxRetries - Maximum number of retries.
6
- * @param {number} initialDelay - Initial delay in ms for backoff.
7
- */
1
+ /** Fetches a URL with retry functionality [ds] */
8
2
  async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
9
3
  let lastError;
10
4
  for (let attempt = 0; attempt < maxRetries; attempt++) {
11
- // AbortController to handle request timeouts
12
5
  const controller = new AbortController();
13
6
  const timeoutId = setTimeout(() => controller.abort(), 15000);
14
7
  try {
@@ -17,11 +10,9 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
17
10
  signal: controller.signal
18
11
  });
19
12
  clearTimeout(timeoutId);
20
- // If response is valid, return immediately
21
13
  if (response.ok) {
22
14
  return response;
23
15
  }
24
- // Retry on rate limit (429) or server-side errors (500-599)
25
16
  if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
26
17
  lastError = new Error(`HTTP Error ${response.status}: ${response.statusText}`);
27
18
  } else {
@@ -29,7 +20,6 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
29
20
  }
30
21
  } catch (err) {
31
22
  clearTimeout(timeoutId);
32
- // Handle specific timeout case separately
33
23
  if (err.name === 'AbortError') {
34
24
  lastError = new Error("Request timed out after 15 seconds");
35
25
  } else {
@@ -38,7 +28,6 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
38
28
  }
39
29
 
40
30
  if (attempt < maxRetries - 1) {
41
- // Calculate exponential backoff delay
42
31
  const backoffDelay = initialDelay * Math.pow(2, attempt);
43
32
  console.warn(`[devsplain] AI request failed. Retrying in ${backoffDelay}ms... (Attempt ${attempt + 1}/${maxRetries})`);
44
33
  await new Promise(resolve => setTimeout(resolve, backoffDelay));
@@ -47,15 +36,8 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
47
36
  throw lastError;
48
37
  }
49
38
 
50
- /**
51
- * Generates documentation for code by sending it to an LLM provider.
52
- * @param {string} code - The source code to document.
53
- * @param {string} language - The programming language.
54
- * @param {object} config - Provider configuration (apiKey, model, etc.).
55
- * @param {string} mode - Operation mode (default, clean, light, full).
56
- */
39
+ /** Gets comments for the given code [ds] */
57
40
  async function getComments(code, language, config, mode = 'default') {
58
- // Split into lines and prepend line numbers for LLM context
59
41
  const lines = code.split(/\r?\n/);
60
42
  const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
61
43
 
@@ -110,9 +92,10 @@ ${numberedCode}
110
92
  `.trim();
111
93
  }
112
94
 
95
+ // Initialize text response [ds]
113
96
  let textResponse = "";
114
97
 
115
- // Branching logic for different LLM providers
98
+ // Check if provider is Gemini [ds]
116
99
  if (config.provider === 'gemini') {
117
100
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
118
101
  let data;
@@ -135,6 +118,7 @@ ${numberedCode}
135
118
  }
136
119
  textResponse = data.candidates[0].content.parts[0].text;
137
120
  }
121
+ // Otherwise, use a different provider [ds]
138
122
  else {
139
123
  const url = `${config.baseUrl}/v1/chat/completions`;
140
124
  let data;
@@ -164,7 +148,7 @@ ${numberedCode}
164
148
  textResponse = data.choices[0].message.content;
165
149
  }
166
150
 
167
- // Extract JSON array from LLM response text
151
+ // Clean up the text response [ds]
168
152
  let cleanText = textResponse.trim();
169
153
  const start = cleanText.indexOf('[');
170
154
  const end = cleanText.lastIndexOf(']');
@@ -172,8 +156,8 @@ ${numberedCode}
172
156
  cleanText = cleanText.substring(start, end + 1);
173
157
  }
174
158
 
159
+ // Parse the response as JSON [ds]
175
160
  let parsed;
176
- // Validate response format and schema integrity
177
161
  try {
178
162
  parsed = JSON.parse(cleanText);
179
163
  } catch (e) {
@@ -184,6 +168,7 @@ ${numberedCode}
184
168
  throw new Error("Schema Error: LLM response is not a JSON array.");
185
169
  }
186
170
 
171
+ // Validate the parsed response [ds]
187
172
  for (const item of parsed) {
188
173
  if (typeof item !== 'object' || item === null) {
189
174
  throw new Error("Schema Error: Array elements must be objects.");
@@ -202,7 +187,6 @@ ${numberedCode}
202
187
  }
203
188
 
204
189
  const trimmedComment = item.comment.trim();
205
- // Sanity check for valid comment syntax
206
190
  const startsWithCommentMarker =
207
191
  trimmedComment.startsWith('//') ||
208
192
  trimmedComment.startsWith('/*') ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",