devsplain 1.5.2 → 1.5.4

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
@@ -22,7 +22,7 @@ function isGitDirty() {
22
22
  return false;
23
23
  }
24
24
 
25
- /** Determines if a specific line index falls within a multi-line string or block */
25
+ /** Determines if a specific line index falls within a string literal */
26
26
  function isLineInsideString(lines, targetLineIndex, ext = '') {
27
27
  const isPython = ext.toLowerCase() === '.py';
28
28
  let inBacktick = false;
@@ -31,6 +31,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
31
31
  let inSingle = false;
32
32
  let inDouble = false;
33
33
 
34
+ // Iterate through lines prior to the target to track string/block state
34
35
  for (let i = 0; i < targetLineIndex; i++) {
35
36
  const line = lines[i];
36
37
  let j = 0;
@@ -51,6 +52,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
51
52
  }
52
53
  }
53
54
  } else {
55
+ // Check for unescaped backtick (JS template strings) or quotes
54
56
  if (!inSingle && !inDouble && line[j] === '`') {
55
57
  let escaped = false;
56
58
  let k = j - 1;
@@ -96,7 +98,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
96
98
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
97
99
  }
98
100
 
99
- /** Analyzes code to determine which lines are purely comments */
101
+ /** Performs a lexical analysis to categorize code lines and comment blocks */
100
102
  function analyzeComments(lines, ext = '') {
101
103
  const isPython = ext.toLowerCase() === '.py';
102
104
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -108,6 +110,7 @@ function analyzeComments(lines, ext = '') {
108
110
  let inDouble = false;
109
111
  let inBlockJS = false;
110
112
  let inBlockHTML = false;
113
+ // Iterate through each line character by character to detect comment boundaries
111
114
  for (let i = 0; i < lines.length; i++) {
112
115
  const line = lines[i];
113
116
  let commentStartIndex = -1;
@@ -247,19 +250,19 @@ function analyzeComments(lines, ext = '') {
247
250
  return analysis;
248
251
  }
249
252
 
250
- /** Splices AI-generated comments into source code or cleans existing ones */
253
+ /** Splices generated comments into the source data or removes existing ones */
251
254
  function spliceComments(data, comments, mode = 'default', ext = '') {
255
+ // Determine platform-specific line endings
252
256
  const hasCRLF = data.includes('\r\n');
253
257
  const lineEnding = hasCRLF ? '\r\n' : '\n';
254
258
  const originalLines = data.split(/\r?\n/);
255
259
  const sortedComments = [...comments].sort((a, b) => b.line - a.line);
256
260
  const validComments = sortedComments.filter(c => c.line >= 1 && c.line <= originalLines.length + 1);
257
261
 
258
- // Map lines to objects to track original positioning after splicing
259
262
  const annotated = originalLines.map((text, index) => ({ text, originalIndex: index }));
260
263
  let analysis = null;
261
264
 
262
- // Logic for removing existing comments
265
+ // 'clean' mode removes all existing comments/documentation
263
266
  if (mode === 'clean') {
264
267
  analysis = analyzeComments(originalLines, ext);
265
268
  const finalDeletions = new Set();
@@ -281,7 +284,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
281
284
 
282
285
  const linesToDelete = Array.from(finalDeletions).sort((a, b) => b - a);
283
286
 
284
- // Process deletions in reverse to maintain line integrity
285
287
  for (const lineNum of linesToDelete) {
286
288
  const targetLine = originalLines[lineNum - 1];
287
289
  if (!targetLine) continue;
@@ -309,6 +311,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
309
311
  annotated.splice(lineNum - 1, 1);
310
312
  }
311
313
  } else {
314
+ // 'default'/'light'/'full' mode: Inject AI-generated comments
312
315
  for (const c of validComments) {
313
316
  if (isLineInsideString(originalLines, c.line - 1, ext)) {
314
317
  console.warn(`[devsplain] Skipping comment insertion at line ${c.line} to avoid string literal corruption.`);
@@ -316,7 +319,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
316
319
  }
317
320
 
318
321
  const targetLine = originalLines[c.line - 1] || '';
319
- // Determine indentation level for new comment blocks
320
322
  const indentMatch = targetLine.match(/^([ \t]*)/);
321
323
  const indentation = indentMatch ? indentMatch[1] : '';
322
324
 
@@ -334,7 +336,6 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
334
336
  }
335
337
  }
336
338
 
337
- // Verify that the result matches expected output before committing to disk
338
339
  const filtered = annotated.filter(line => line.originalIndex !== -1);
339
340
  const filteredText = filtered.map(line => line.text);
340
341
  const filteredIndices = filtered.map(line => line.originalIndex);
@@ -373,7 +374,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
373
374
  return annotated.map(line => line.text).join(lineEnding);
374
375
  }
375
376
 
376
- /** Main CLI execution loop */
377
+ /** Main entry point for the CLI tool logic */
377
378
  async function runCLI() {
378
379
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
379
380
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -490,7 +491,7 @@ Options:
490
491
  let successCount = 0;
491
492
  let failCount = 0;
492
493
 
493
- /** Recursively process files or directories */
494
+ /** Recursively traverses the file system to identify and process source files */
494
495
  async function processPath(targetPath) {
495
496
  const stats = fs.statSync(targetPath);
496
497
 
@@ -534,8 +535,8 @@ Options:
534
535
  }
535
536
 
536
537
  console.log(` Analyzing ${filename} in ${mode} mode...`);
537
- // Perform comment generation if not in 'clean' mode
538
538
  try {
539
+ // Logic to either clean existing comments or replace/insert new ones
539
540
  let comments = [];
540
541
  let commentedCode;
541
542
  if (mode !== 'clean') {
@@ -551,7 +552,7 @@ Options:
551
552
  console.log(`---------------------------------------\n`);
552
553
  const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
553
554
  if (answer.toLowerCase() === 'write') {
554
- // Atomic write: write to temp file then rename
555
+ // Use temporary file for atomic write operations
555
556
  const tempPath = targetPath + '.tmp';
556
557
  fs.writeFileSync(tempPath, commentedCode, 'utf8');
557
558
  fs.renameSync(tempPath, targetPath);
@@ -589,7 +590,6 @@ Options:
589
590
  rl.close();
590
591
  }
591
592
 
592
- // Execute main if run directly, otherwise export utility functions
593
593
  if (require.main === module) {
594
594
  runCLI().catch(err => {
595
595
  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.2",
3
+ "version": "1.5.4",
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",