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 +14 -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
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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);
|
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