devsplain 1.5.5 → 1.6.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 +8 -3
- package/bin/cli.js +100 -21
- package/bin/post-commit.js +11 -2
- package/bin/setup-hook.js +15 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ An industrial-grade, agent-agnostic CLI tool that automatically adds JSDoc and i
|
|
|
8
8
|
|
|
9
9
|
- **Mathematical Safety Invariants**: Uses an index-preserving splicing engine. Your functional code is mathematically verified to remain identical before and after commenting.
|
|
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
|
+
- **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.
|
|
12
13
|
- **Git Hook Automation**: Supports an automated two-commit Git hook workflow (`pre-commit` for quality, `post-commit` for auto-generated documentation commits) that prevents recursive commit loops.
|
|
13
14
|
- **Bring Your Own LLM**: Native setup wizard for Groq, Gemini, OpenAI, or any OpenAI-compatible API endpoint (like Ollama or LMStudio).
|
|
14
15
|
- **Exponential Backoff**: Resilient AI request handler that automatically retries rate-limited requests with exponential backoff.
|
|
@@ -78,7 +79,8 @@ devsplain <file-or-directory> [options]
|
|
|
78
79
|
| `--full` | Aggressive commenting. Explains complex logic blocks line-by-line inside functions. |
|
|
79
80
|
| `--dry-run` | Preview comments in the terminal without writing to files. Prompts for manual save confirmation. |
|
|
80
81
|
| `--force` | Bypasses the safety block check that prevents running `devsplain` on a dirty Git working tree. |
|
|
81
|
-
| `--clean` | Scrubber mode. Deterministically removes
|
|
82
|
+
| `--clean` | Scrubber mode. Deterministically removes only devsplain-generated comments tagged with `[ds]`, preserving your manual comments. |
|
|
83
|
+
| `--prune` | Destructive scrubber mode. Removes ALL comments and docstrings from source files, including your own manual comments. |
|
|
82
84
|
| `--provider <name>`| Temporary one-off override for the AI provider (`gemini`, `groq`, `openai`, `custom`) for this command run only (does not modify the saved config file). |
|
|
83
85
|
| `--model <name>` | Temporary one-off override for the model name for this command run only. |
|
|
84
86
|
| `--api-key <key>` | Temporary one-off override for the API key for this command run only. |
|
|
@@ -97,9 +99,12 @@ devsplain src/index.js --light
|
|
|
97
99
|
# Deep logic commenting on a folder (skips node_modules, .git, etc.)
|
|
98
100
|
devsplain src/ --full
|
|
99
101
|
|
|
100
|
-
# Clean and scrub comments
|
|
102
|
+
# Clean and scrub AI-generated comments locally without API calls
|
|
101
103
|
devsplain lib/ --clean
|
|
102
104
|
|
|
105
|
+
# Destructively remove ALL comments (both AI and manual) from a folder
|
|
106
|
+
devsplain lib/ --prune
|
|
107
|
+
|
|
103
108
|
# Headless run using overriding credentials
|
|
104
109
|
devsplain src/utils.ts --provider gemini --model gemini-2.0-flash --api-key YOUR_KEY
|
|
105
110
|
```
|
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
|
|
13
|
+
/** Checks if the current Git repository is dirty by inspecting status. */
|
|
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,7 +23,8 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Determines if a line is within a string literal
|
|
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). */
|
|
27
28
|
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
28
29
|
const isPython = ext.toLowerCase() === '.py';
|
|
29
30
|
let inBacktick = false;
|
|
@@ -97,7 +98,8 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
97
98
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
/**
|
|
101
|
+
/** Parses a file to identify pure comments and block structures. [ds] */
|
|
102
|
+
/** Parses a file to identify pure comments and block structures. */
|
|
101
103
|
function analyzeComments(lines, ext = '') {
|
|
102
104
|
const isPython = ext.toLowerCase() === '.py';
|
|
103
105
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -248,7 +250,8 @@ function analyzeComments(lines, ext = '') {
|
|
|
248
250
|
return analysis;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
/**
|
|
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. */
|
|
252
255
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
253
256
|
const hasCRLF = data.includes('\r\n');
|
|
254
257
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -258,19 +261,72 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
258
261
|
|
|
259
262
|
const annotated = originalLines.map((text, index) => ({ text, originalIndex: index }));
|
|
260
263
|
let analysis = null;
|
|
264
|
+
let dsBlocks = new Set();
|
|
261
265
|
|
|
262
|
-
if (mode === 'clean') {
|
|
266
|
+
if (mode === 'clean' || mode === 'prune') {
|
|
263
267
|
analysis = analyzeComments(originalLines, ext);
|
|
264
268
|
const finalDeletions = new Set();
|
|
269
|
+
|
|
270
|
+
if (mode === 'clean') {
|
|
271
|
+
let i = 0;
|
|
272
|
+
while (i < originalLines.length) {
|
|
273
|
+
if (analysis[i].isInsideBlock) {
|
|
274
|
+
let start = i;
|
|
275
|
+
let end = i;
|
|
276
|
+
while (end < originalLines.length && analysis[end].isInsideBlock) end++;
|
|
277
|
+
|
|
278
|
+
let blockStart = start - 1;
|
|
279
|
+
let blockEnd = end - 1;
|
|
280
|
+
|
|
281
|
+
let hasDs = false;
|
|
282
|
+
for (let k = blockStart; k <= blockEnd; k++) {
|
|
283
|
+
if (originalLines[k].includes('[ds]')) hasDs = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (hasDs) {
|
|
287
|
+
for (let k = blockStart; k <= blockEnd; k++) {
|
|
288
|
+
dsBlocks.add(k + 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
i = end;
|
|
292
|
+
} else {
|
|
293
|
+
i++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
265
298
|
for (let i = 0; i < originalLines.length; i++) {
|
|
266
299
|
const lineNum = i + 1;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
300
|
+
const lineStr = originalLines[i];
|
|
301
|
+
const lineAnalysis = analysis[i];
|
|
302
|
+
|
|
303
|
+
if (lineStr.trim().startsWith('#!')) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (mode === 'prune') {
|
|
308
|
+
if (lineAnalysis.isPureComment) {
|
|
309
|
+
finalDeletions.add(lineNum);
|
|
310
|
+
} else if (lineAnalysis.commentStartIndex !== -1) {
|
|
311
|
+
annotated[i].text = lineStr.slice(0, lineAnalysis.commentStartIndex).trimEnd();
|
|
312
|
+
}
|
|
313
|
+
} else if (mode === 'clean') {
|
|
314
|
+
const isDsBlockLine = dsBlocks.has(lineNum);
|
|
315
|
+
const hasDsInline = lineStr.includes('[ds]');
|
|
316
|
+
|
|
317
|
+
if (lineAnalysis.isPureComment) {
|
|
318
|
+
if (isDsBlockLine || hasDsInline) {
|
|
319
|
+
finalDeletions.add(lineNum);
|
|
320
|
+
}
|
|
321
|
+
} else if (lineAnalysis.commentStartIndex !== -1) {
|
|
322
|
+
if (isDsBlockLine || hasDsInline) {
|
|
323
|
+
annotated[i].text = lineStr.slice(0, lineAnalysis.commentStartIndex).trimEnd();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
271
326
|
}
|
|
272
327
|
}
|
|
273
328
|
|
|
329
|
+
|
|
274
330
|
for (const c of validComments) {
|
|
275
331
|
const lineIdx = c.line - 1;
|
|
276
332
|
if (lineIdx >= 0 && lineIdx < originalLines.length) {
|
|
@@ -321,9 +377,23 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
321
377
|
const indentMatch = targetLine.match(/^([ \t]*)/);
|
|
322
378
|
const indentation = indentMatch ? indentMatch[1] : '';
|
|
323
379
|
|
|
324
|
-
const commentLines = c.comment.split(/\r?\n/).map(line => {
|
|
325
|
-
|
|
380
|
+
const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
|
|
381
|
+
let trimmed = line.trimStart();
|
|
326
382
|
if (!trimmed) return '';
|
|
383
|
+
|
|
384
|
+
const isSingleLine = trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('--');
|
|
385
|
+
const isBlockEnd = trimmed.endsWith('*/') || trimmed.endsWith('-->');
|
|
386
|
+
|
|
387
|
+
if (isSingleLine) {
|
|
388
|
+
trimmed = trimmed + ' [ds]';
|
|
389
|
+
} else if (idx === 0) {
|
|
390
|
+
if (isBlockEnd) {
|
|
391
|
+
trimmed = trimmed.replace(/(\*\/|-->)$/, '[ds] $1');
|
|
392
|
+
} else {
|
|
393
|
+
trimmed = trimmed + ' [ds]';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
327
397
|
if (trimmed.startsWith('*') && !trimmed.startsWith('*/') && !trimmed.startsWith('/*')) {
|
|
328
398
|
return indentation + ' ' + trimmed;
|
|
329
399
|
}
|
|
@@ -345,12 +415,17 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
345
415
|
if (text === originalLine) {
|
|
346
416
|
return true;
|
|
347
417
|
}
|
|
348
|
-
if (mode === 'clean' && analysis) {
|
|
418
|
+
if ((mode === 'clean' || mode === 'prune') && analysis) {
|
|
349
419
|
const lineAnalysis = analysis[origIdx];
|
|
350
420
|
if (lineAnalysis && lineAnalysis.commentStartIndex !== -1 && !lineAnalysis.isPureComment) {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
421
|
+
const isDsBlockLine = dsBlocks.has(origIdx + 1);
|
|
422
|
+
const hasDsInline = originalLine.includes('[ds]');
|
|
423
|
+
|
|
424
|
+
if (mode === 'prune' || (mode === 'clean' && (hasDsInline || isDsBlockLine))) {
|
|
425
|
+
const expectedStripped = originalLine.slice(0, lineAnalysis.commentStartIndex).trimEnd();
|
|
426
|
+
if (text === expectedStripped) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
354
429
|
}
|
|
355
430
|
}
|
|
356
431
|
}
|
|
@@ -373,7 +448,8 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
373
448
|
return annotated.map(line => line.text).join(lineEnding);
|
|
374
449
|
}
|
|
375
450
|
|
|
376
|
-
/** Main CLI
|
|
451
|
+
/** Main entry point for the CLI tool. [ds] */
|
|
452
|
+
/** Main entry point for the CLI tool. */
|
|
377
453
|
async function runCLI() {
|
|
378
454
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
379
455
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -392,6 +468,8 @@ Options:
|
|
|
392
468
|
--full Add detailed JSDoc/block comments and inline comments
|
|
393
469
|
--dry-run Preview comments without writing to file
|
|
394
470
|
--force Bypass the dirty Git tree safety check
|
|
471
|
+
--clean Scrub only devsplain-generated [ds] comments
|
|
472
|
+
--prune Destructively scrub ALL comments from files
|
|
395
473
|
--provider <name> Override AI provider (gemini, groq, openai, custom)
|
|
396
474
|
--model <name> Override AI model name
|
|
397
475
|
--api-key <key> Override API key for the provider
|
|
@@ -426,7 +504,7 @@ Options:
|
|
|
426
504
|
return;
|
|
427
505
|
}
|
|
428
506
|
|
|
429
|
-
// Helper to
|
|
507
|
+
// Helper to extract flag values from command line arguments [ds]
|
|
430
508
|
const getArgValue = (flag) => {
|
|
431
509
|
const index = args.indexOf(flag);
|
|
432
510
|
if (index !== -1 && index + 1 < args.length) {
|
|
@@ -459,6 +537,7 @@ Options:
|
|
|
459
537
|
if (args.includes('--light')) mode = 'light';
|
|
460
538
|
if (args.includes('--full')) mode = 'full';
|
|
461
539
|
if (args.includes('--clean')) mode = 'clean';
|
|
540
|
+
if (args.includes('--prune')) mode = 'prune';
|
|
462
541
|
const isDryRun = args.includes('--dry-run');
|
|
463
542
|
const isForce = args.includes('--force');
|
|
464
543
|
|
|
@@ -491,7 +570,8 @@ Options:
|
|
|
491
570
|
let successCount = 0;
|
|
492
571
|
let failCount = 0;
|
|
493
572
|
|
|
494
|
-
/** Recursively processes files or directories to apply comments */
|
|
573
|
+
/** Recursively processes files or directories to apply AI-generated comments. [ds] */
|
|
574
|
+
/** Recursively processes files or directories to apply AI-generated comments. */
|
|
495
575
|
async function processPath(targetPath) {
|
|
496
576
|
const stats = fs.statSync(targetPath);
|
|
497
577
|
|
|
@@ -504,7 +584,6 @@ Options:
|
|
|
504
584
|
'.vscode', '.idea', 'coverage'
|
|
505
585
|
];
|
|
506
586
|
|
|
507
|
-
// Skip common dependency and configuration folders
|
|
508
587
|
if (ignoredFolders.includes(folderName)) {
|
|
509
588
|
return;
|
|
510
589
|
}
|
|
@@ -539,7 +618,6 @@ Options:
|
|
|
539
618
|
try {
|
|
540
619
|
let comments = [];
|
|
541
620
|
let commentedCode;
|
|
542
|
-
// Perform comment processing: Clean existing, then inject new comments via LLM
|
|
543
621
|
if (mode !== 'clean') {
|
|
544
622
|
const cleanData = spliceComments(data, [], 'clean', ext);
|
|
545
623
|
comments = await getComments(cleanData, filename, config, mode);
|
|
@@ -590,7 +668,8 @@ Options:
|
|
|
590
668
|
rl.close();
|
|
591
669
|
}
|
|
592
670
|
|
|
593
|
-
//
|
|
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
|
|
594
673
|
if (require.main === module) {
|
|
595
674
|
runCLI().catch(err => {
|
|
596
675
|
console.error(err);
|
package/bin/post-commit.js
CHANGED
|
@@ -3,14 +3,17 @@ 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] */
|
|
6
7
|
/** Main execution block to detect changes and process documentation generation */
|
|
7
8
|
try {
|
|
8
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]
|
|
9
11
|
const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
|
|
10
12
|
if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
|
|
11
13
|
process.exit(0);
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
// Get the list of file paths modified in the current commit [ds]
|
|
14
17
|
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
|
|
15
18
|
if (!changedFilesStr) {
|
|
16
19
|
process.exit(0);
|
|
@@ -18,6 +21,7 @@ try {
|
|
|
18
21
|
// Retrieve list of files modified in the latest commit
|
|
19
22
|
const changedFiles = changedFilesStr.split(/\r?\n/);
|
|
20
23
|
|
|
24
|
+
// Define allowed file types for documentation processing [ds]
|
|
21
25
|
const validExtensions = [
|
|
22
26
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
23
27
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
@@ -37,6 +41,7 @@ try {
|
|
|
37
41
|
console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
|
|
38
42
|
|
|
39
43
|
// Parse command line arguments to determine documentation verbosity mode
|
|
44
|
+
// Parse CLI arguments for verbosity preferences [ds]
|
|
40
45
|
const args = process.argv.slice(2);
|
|
41
46
|
let modeFlag = '';
|
|
42
47
|
if (args.includes('--light')) modeFlag = ' --light';
|
|
@@ -51,6 +56,7 @@ try {
|
|
|
51
56
|
const contentHead = fs.readFileSync(file, 'utf8');
|
|
52
57
|
let contentPrev = '';
|
|
53
58
|
try {
|
|
59
|
+
// Attempt to retrieve the file version from the previous commit for change comparison [ds]
|
|
54
60
|
// Attempt to fetch the file content from the previous commit state for comparison
|
|
55
61
|
contentPrev = execSync(`git show HEAD~1:"${file}"`, {
|
|
56
62
|
encoding: 'utf8',
|
|
@@ -60,9 +66,10 @@ try {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
if (contentPrev) {
|
|
69
|
+
// Prune comments to isolate actual code changes and ignore documentation-only commits [ds]
|
|
63
70
|
// Strip comments from head and previous versions to detect if logic actually changed
|
|
64
|
-
const cleanHead = spliceComments(contentHead, [], '
|
|
65
|
-
const cleanPrev = spliceComments(contentPrev, [], '
|
|
71
|
+
const cleanHead = spliceComments(contentHead, [], 'prune', ext);
|
|
72
|
+
const cleanPrev = spliceComments(contentPrev, [], 'prune', ext);
|
|
66
73
|
// Skip processing if only comments were modified in the commit
|
|
67
74
|
if (cleanHead === cleanPrev) {
|
|
68
75
|
console.log(`[devsplain] Skipping ${file}: commit contains only comment changes.`);
|
|
@@ -75,6 +82,7 @@ try {
|
|
|
75
82
|
console.log(`[devsplain] Automatically commenting file: ${file}`);
|
|
76
83
|
try {
|
|
77
84
|
// Execute the CLI generator for the specific file
|
|
85
|
+
// Path to the underlying documentation generation engine [ds]
|
|
78
86
|
const cliPath = path.join(__dirname, 'cli.js');
|
|
79
87
|
execSync(`node "${cliPath}" "${file}" --force${modeFlag}`, { stdio: 'inherit' });
|
|
80
88
|
commentedAny = true;
|
|
@@ -83,6 +91,7 @@ try {
|
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
// Stage changes back to the repo if new documentation was generated [ds]
|
|
86
95
|
// If changes were made by the generator, stage and commit the result back to the repository
|
|
87
96
|
if (commentedAny) {
|
|
88
97
|
const status = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
package/bin/setup-hook.js
CHANGED
|
@@ -5,25 +5,25 @@ const readline = require('readline');
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Orchestrates the installation of Git pre-commit and post-commit hooks.
|
|
8
|
-
*
|
|
8
|
+
* Sets up necessary directories and writes hook files with user-selected configuration.
|
|
9
9
|
*/
|
|
10
10
|
async function installHooks() {
|
|
11
11
|
try {
|
|
12
|
-
// Determine the path
|
|
12
|
+
// Determine the actual git directory path using git command line tool
|
|
13
13
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
14
14
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
15
|
+
// Ensure the hooks directory exists before attempting to write files
|
|
15
16
|
if (!fs.existsSync(hooksDir)) {
|
|
16
17
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
let modeChoice = '1';
|
|
20
|
-
//
|
|
21
|
+
// Prompt the user for mode selection only if running in an interactive terminal session
|
|
21
22
|
if (process.stdout.isTTY) {
|
|
22
23
|
const rl = readline.createInterface({
|
|
23
24
|
input: process.stdin,
|
|
24
25
|
output: process.stdout
|
|
25
26
|
});
|
|
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
|
-
// Map
|
|
38
|
+
// Map user input to CLI arguments for the post-commit script execution
|
|
39
39
|
let modeArgs = '';
|
|
40
40
|
if (modeChoice === '2') {
|
|
41
41
|
modeArgs = ' --light';
|
|
@@ -43,23 +43,25 @@ async function installHooks() {
|
|
|
43
43
|
modeArgs = ' --full';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// Generate and write the pre-commit shell script to trigger tests before committing
|
|
47
47
|
const preCommitHookPath = path.join(hooksDir, 'pre-commit');
|
|
48
48
|
const preCommitContent = `#!/bin/sh
|
|
49
49
|
# devsplain native pre-commit hook
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
|
|
51
|
+
echo "Running pre-commit tests..."
|
|
52
|
+
npm test || exit 1
|
|
53
|
+
fi
|
|
52
54
|
`;
|
|
53
55
|
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
54
56
|
try {
|
|
55
|
-
//
|
|
57
|
+
// Apply execute permissions to the hook file
|
|
56
58
|
fs.chmodSync(preCommitHookPath, 0o755);
|
|
57
59
|
} catch (err) {}
|
|
58
60
|
|
|
59
|
-
//
|
|
61
|
+
// Normalize the path for cross-platform compatibility when injecting into shell script
|
|
60
62
|
const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
|
|
61
63
|
|
|
62
|
-
//
|
|
64
|
+
// Generate and write the post-commit shell script to execute the documentation generation
|
|
63
65
|
const postCommitHookPath = path.join(hooksDir, 'post-commit');
|
|
64
66
|
const postCommitContent = `#!/bin/sh
|
|
65
67
|
# devsplain native post-commit hook
|
|
@@ -67,8 +69,8 @@ echo "Auto-generating comments for files in the last commit..."
|
|
|
67
69
|
node "${postCommitScript}"${modeArgs} || exit 1
|
|
68
70
|
`;
|
|
69
71
|
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
72
|
+
// Apply execute permissions to the hook file
|
|
70
73
|
try {
|
|
71
|
-
// Ensure the hook file is executable
|
|
72
74
|
fs.chmodSync(postCommitHookPath, 0o755);
|
|
73
75
|
} catch (err) {}
|
|
74
76
|
|
|
@@ -78,6 +80,7 @@ node "${postCommitScript}"${modeArgs} || exit 1
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
// Execute installation automatically if this script is run as the entry point
|
|
81
84
|
if (require.main === module) {
|
|
82
85
|
installHooks();
|
|
83
86
|
}
|
package/package.json
CHANGED