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 +13 -13
- package/bin/post-commit.js +13 -12
- package/bin/setup-hook.js +13 -10
- package/package.json +1 -1
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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);
|
package/bin/post-commit.js
CHANGED
|
@@ -3,10 +3,10 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spliceComments } = require('./cli');
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
77
|
-
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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