devsplain 1.5.3 → 1.5.5

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
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
 
2
3
  const { getComments } = require('../lib/llm.js');
3
4
  const { getConfig } = require('../lib/config.js');
@@ -22,7 +23,7 @@ function isGitDirty() {
22
23
  return false;
23
24
  }
24
25
 
25
- /** Determines if a specific line index falls within a string literal */
26
+ /** Determines if a line is within a string literal in the source code */
26
27
  function isLineInsideString(lines, targetLineIndex, ext = '') {
27
28
  const isPython = ext.toLowerCase() === '.py';
28
29
  let inBacktick = false;
@@ -31,7 +32,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
31
32
  let inSingle = false;
32
33
  let inDouble = false;
33
34
 
34
- // Iterate through lines prior to the target to track string/block state
35
35
  for (let i = 0; i < targetLineIndex; i++) {
36
36
  const line = lines[i];
37
37
  let j = 0;
@@ -52,7 +52,6 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
52
52
  }
53
53
  }
54
54
  } else {
55
- // Check for unescaped backtick (JS template strings) or quotes
56
55
  if (!inSingle && !inDouble && line[j] === '`') {
57
56
  let escaped = false;
58
57
  let k = j - 1;
@@ -98,7 +97,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
98
97
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
99
98
  }
100
99
 
101
- /** Performs a lexical analysis to categorize code lines and comment blocks */
100
+ /** Analyzes source code to identify comments and code blocks */
102
101
  function analyzeComments(lines, ext = '') {
103
102
  const isPython = ext.toLowerCase() === '.py';
104
103
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -110,7 +109,6 @@ function analyzeComments(lines, ext = '') {
110
109
  let inDouble = false;
111
110
  let inBlockJS = false;
112
111
  let inBlockHTML = false;
113
- // Iterate through each line character by character to detect comment boundaries
114
112
  for (let i = 0; i < lines.length; i++) {
115
113
  const line = lines[i];
116
114
  let commentStartIndex = -1;
@@ -250,9 +248,8 @@ function analyzeComments(lines, ext = '') {
250
248
  return analysis;
251
249
  }
252
250
 
253
- /** Splices generated comments into the source data or removes existing ones */
251
+ /** Applies or removes comments from source data based on a specified mode */
254
252
  function spliceComments(data, comments, mode = 'default', ext = '') {
255
- // Determine platform-specific line endings
256
253
  const hasCRLF = data.includes('\r\n');
257
254
  const lineEnding = hasCRLF ? '\r\n' : '\n';
258
255
  const originalLines = data.split(/\r?\n/);
@@ -262,7 +259,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
262
259
  const annotated = originalLines.map((text, index) => ({ text, originalIndex: index }));
263
260
  let analysis = null;
264
261
 
265
- // 'clean' mode removes all existing comments/documentation
266
262
  if (mode === 'clean') {
267
263
  analysis = analyzeComments(originalLines, ext);
268
264
  const finalDeletions = new Set();
@@ -290,6 +286,10 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
290
286
  const trimmedLine = targetLine.trim();
291
287
 
292
288
  const lineAnalysis = analysis[lineNum - 1];
289
+ if (trimmedLine.startsWith('#!')) {
290
+ continue;
291
+ }
292
+
293
293
  const isCommentLine =
294
294
  lineAnalysis.isInsideBlock ||
295
295
  lineAnalysis.isPureComment ||
@@ -311,7 +311,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
311
311
  annotated.splice(lineNum - 1, 1);
312
312
  }
313
313
  } else {
314
- // 'default'/'light'/'full' mode: Inject AI-generated comments
315
314
  for (const c of validComments) {
316
315
  if (isLineInsideString(originalLines, c.line - 1, ext)) {
317
316
  console.warn(`[devsplain] Skipping comment insertion at line ${c.line} to avoid string literal corruption.`);
@@ -374,7 +373,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
374
373
  return annotated.map(line => line.text).join(lineEnding);
375
374
  }
376
375
 
377
- /** Main entry point for the CLI tool logic */
376
+ /** Main CLI execution logic */
378
377
  async function runCLI() {
379
378
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
380
379
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -427,6 +426,7 @@ Options:
427
426
  return;
428
427
  }
429
428
 
429
+ // Helper to retrieve specific CLI argument values
430
430
  const getArgValue = (flag) => {
431
431
  const index = args.indexOf(flag);
432
432
  if (index !== -1 && index + 1 < args.length) {
@@ -491,7 +491,7 @@ Options:
491
491
  let successCount = 0;
492
492
  let failCount = 0;
493
493
 
494
- /** Recursively traverses the file system to identify and process source files */
494
+ /** Recursively processes files or directories to apply comments */
495
495
  async function processPath(targetPath) {
496
496
  const stats = fs.statSync(targetPath);
497
497
 
@@ -504,6 +504,7 @@ Options:
504
504
  '.vscode', '.idea', 'coverage'
505
505
  ];
506
506
 
507
+ // Skip common dependency and configuration folders
507
508
  if (ignoredFolders.includes(folderName)) {
508
509
  return;
509
510
  }
@@ -536,9 +537,9 @@ Options:
536
537
 
537
538
  console.log(` Analyzing ${filename} in ${mode} mode...`);
538
539
  try {
539
- // Logic to either clean existing comments or replace/insert new ones
540
540
  let comments = [];
541
541
  let commentedCode;
542
+ // Perform comment processing: Clean existing, then inject new comments via LLM
542
543
  if (mode !== 'clean') {
543
544
  const cleanData = spliceComments(data, [], 'clean', ext);
544
545
  comments = await getComments(cleanData, filename, config, mode);
@@ -552,7 +553,6 @@ Options:
552
553
  console.log(`---------------------------------------\n`);
553
554
  const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
554
555
  if (answer.toLowerCase() === 'write') {
555
- // Use temporary file for atomic write operations
556
556
  const tempPath = targetPath + '.tmp';
557
557
  fs.writeFileSync(tempPath, commentedCode, 'utf8');
558
558
  fs.renameSync(tempPath, targetPath);
@@ -590,6 +590,7 @@ Options:
590
590
  rl.close();
591
591
  }
592
592
 
593
+ // Initialize the CLI application if executed as a script
593
594
  if (require.main === module) {
594
595
  runCLI().catch(err => {
595
596
  console.error(err);
@@ -3,10 +3,10 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { spliceComments } = require('./cli');
5
5
 
6
- // Wrap logic in try-catch to prevent blocking the git commit process on failure
6
+ /** Main execution block to detect changes and process documentation generation */
7
7
  try {
8
+ // Prevent recursive loops if the previous commit was an automated documentation commit
8
9
  const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
9
- // Avoid infinite loops if this hook triggered the current commit
10
10
  if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
11
11
  process.exit(0);
12
12
  }
@@ -15,16 +15,16 @@ try {
15
15
  if (!changedFilesStr) {
16
16
  process.exit(0);
17
17
  }
18
+ // Retrieve list of files modified in the latest commit
18
19
  const changedFiles = changedFilesStr.split(/\r?\n/);
19
20
 
20
- // Define supported file extensions for auto-documentation
21
21
  const validExtensions = [
22
22
  '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
23
23
  '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
24
24
  '.swift', '.kt', '.dart', '.sh'
25
25
  ];
26
26
 
27
- // Filter changed files to include only supported extensions that currently exist
27
+ // Filter for supported file extensions and ensure the file still exists
28
28
  const filesToComment = changedFiles.filter(file => {
29
29
  const ext = path.extname(file).toLowerCase();
30
30
  return validExtensions.includes(ext) && fs.existsSync(file);
@@ -36,6 +36,7 @@ try {
36
36
 
37
37
  console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
38
38
 
39
+ // Parse command line arguments to determine documentation verbosity mode
39
40
  const args = process.argv.slice(2);
40
41
  let modeFlag = '';
41
42
  if (args.includes('--light')) modeFlag = ' --light';
@@ -43,15 +44,14 @@ try {
43
44
 
44
45
  let commentedAny = false;
45
46
 
46
- // Iterate through each changed file to check if content actually changed
47
+ // Process each valid file to determine if documentation needs updating
47
48
  for (const file of filesToComment) {
48
49
  try {
49
50
  const ext = path.extname(file).toLowerCase();
50
- // Retrieve current file content from filesystem
51
51
  const contentHead = fs.readFileSync(file, 'utf8');
52
52
  let contentPrev = '';
53
- // Attempt to fetch the version of the file from the previous commit for comparison
54
53
  try {
54
+ // Attempt to fetch the file content from the previous commit state for comparison
55
55
  contentPrev = execSync(`git show HEAD~1:"${file}"`, {
56
56
  encoding: 'utf8',
57
57
  stdio: ['ignore', 'pipe', 'ignore']
@@ -60,9 +60,10 @@ try {
60
60
  }
61
61
 
62
62
  if (contentPrev) {
63
- // Strip existing comments to determine if the logic changed or just documentation
63
+ // Strip comments from head and previous versions to detect if logic actually changed
64
64
  const cleanHead = spliceComments(contentHead, [], 'clean', ext);
65
65
  const cleanPrev = spliceComments(contentPrev, [], 'clean', ext);
66
+ // Skip processing if only comments were modified in the commit
66
67
  if (cleanHead === cleanPrev) {
67
68
  console.log(`[devsplain] Skipping ${file}: commit contains only comment changes.`);
68
69
  continue;
@@ -73,20 +74,20 @@ try {
73
74
 
74
75
  console.log(`[devsplain] Automatically commenting file: ${file}`);
75
76
  try {
76
- // Invoke the CLI tool to generate comments for the changed file
77
- execSync(`node bin/cli.js "${file}" --force${modeFlag}`, { stdio: 'inherit' });
77
+ // Execute the CLI generator for the specific file
78
+ const cliPath = path.join(__dirname, 'cli.js');
79
+ execSync(`node "${cliPath}" "${file}" --force${modeFlag}`, { stdio: 'inherit' });
78
80
  commentedAny = true;
79
81
  } catch (err) {
80
82
  console.warn(`[devsplain] Warning: Failed to comment ${file}: ${err.message}`);
81
83
  }
82
84
  }
83
85
 
84
- // If changes were made, stage and commit them automatically to the current branch
86
+ // If changes were made by the generator, stage and commit the result back to the repository
85
87
  if (commentedAny) {
86
88
  const status = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
87
89
  if (status.length > 0) {
88
90
  console.log('[devsplain] Staging and committing auto-generated comments...');
89
- // Use --no-verify to prevent triggering this hook recursively during the commit
90
91
  execSync('git commit -am "docs: auto-generated comments by devsplain" --no-verify', { stdio: 'inherit' });
91
92
  console.log('[devsplain] Comments committed successfully! Rollback via: git reset --hard HEAD~1');
92
93
  }
package/bin/setup-hook.js CHANGED
@@ -4,12 +4,12 @@ const { execSync } = require('child_process');
4
4
  const readline = require('readline');
5
5
 
6
6
  /**
7
- * Initializes and installs Git pre-commit and post-commit hooks.
8
- * Prompts the user for documentation mode configuration.
7
+ * Orchestrates the installation of Git pre-commit and post-commit hooks.
8
+ * Detects the local .git directory and configures hooks with user-specified mode.
9
9
  */
10
10
  async function installHooks() {
11
11
  try {
12
- // Resolve the root .git directory path
12
+ // Determine the path to the .git directory
13
13
  const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
14
14
  const hooksDir = path.join(gitDir, 'hooks');
15
15
  if (!fs.existsSync(hooksDir)) {
@@ -17,13 +17,13 @@ async function installHooks() {
17
17
  }
18
18
 
19
19
  let modeChoice = '1';
20
- // Interactive mode requires a TTY terminal
20
+ // Interact with user via terminal to select documentation verbosity
21
21
  if (process.stdout.isTTY) {
22
22
  const rl = readline.createInterface({
23
23
  input: process.stdin,
24
24
  output: process.stdout
25
25
  });
26
- // Promisify the readline interface for async/await flow
26
+ // Promisify readline to allow async flow control
27
27
  const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
28
28
 
29
29
  console.log('\nSelect default commenting mode for Git commits:');
@@ -35,7 +35,7 @@ async function installHooks() {
35
35
  rl.close();
36
36
  }
37
37
 
38
- // Determine documentation style flags based on user input
38
+ // Map selected mode to command-line arguments for the post-commit script
39
39
  let modeArgs = '';
40
40
  if (modeChoice === '2') {
41
41
  modeArgs = ' --light';
@@ -43,7 +43,7 @@ async function installHooks() {
43
43
  modeArgs = ' --full';
44
44
  }
45
45
 
46
- // Write the pre-commit script to the git hooks directory
46
+ // Create the executable pre-commit script
47
47
  const preCommitHookPath = path.join(hooksDir, 'pre-commit');
48
48
  const preCommitContent = `#!/bin/sh
49
49
  # devsplain native pre-commit hook
@@ -56,15 +56,19 @@ npm test || exit 1
56
56
  fs.chmodSync(preCommitHookPath, 0o755);
57
57
  } catch (err) {}
58
58
 
59
- // Write the post-commit script that triggers documentation generation
59
+ // Locate the source script for post-commit actions
60
+ const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
61
+
62
+ // Create the executable post-commit script that calls the documentation engine
60
63
  const postCommitHookPath = path.join(hooksDir, 'post-commit');
61
64
  const postCommitContent = `#!/bin/sh
62
65
  # devsplain native post-commit hook
63
66
  echo "Auto-generating comments for files in the last commit..."
64
- node bin/post-commit.js${modeArgs} || exit 1
67
+ node "${postCommitScript}"${modeArgs} || exit 1
65
68
  `;
66
69
  fs.writeFileSync(postCommitHookPath, postCommitContent);
67
70
  try {
71
+ // Ensure the hook file is executable
68
72
  fs.chmodSync(postCommitHookPath, 0o755);
69
73
  } catch (err) {}
70
74
 
@@ -74,7 +78,6 @@ node bin/post-commit.js${modeArgs} || exit 1
74
78
  }
75
79
  }
76
80
 
77
- // Execute the function automatically if the file is run directly
78
81
  if (require.main === module) {
79
82
  installHooks();
80
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
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",