devsplain 1.5.6 → 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 +91 -12
- 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
|
@@ -23,6 +23,7 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Determines if a specific line index is within a string literal (handling quotes/backticks). [ds] */
|
|
26
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';
|
|
@@ -97,6 +98,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
97
98
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/** Parses a file to identify pure comments and block structures. [ds] */
|
|
100
102
|
/** Parses a file to identify pure comments and block structures. */
|
|
101
103
|
function analyzeComments(lines, ext = '') {
|
|
102
104
|
const isPython = ext.toLowerCase() === '.py';
|
|
@@ -248,6 +250,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
248
250
|
return analysis;
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
/** Splices comments into code or cleans existing ones, with safety checks. [ds] */
|
|
251
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');
|
|
@@ -258,22 +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
|
-
|
|
300
|
+
const lineStr = originalLines[i];
|
|
301
|
+
const lineAnalysis = analysis[i];
|
|
302
|
+
|
|
303
|
+
if (lineStr.trim().startsWith('#!')) {
|
|
268
304
|
continue;
|
|
269
305
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
326
|
}
|
|
275
327
|
}
|
|
276
328
|
|
|
329
|
+
|
|
277
330
|
for (const c of validComments) {
|
|
278
331
|
const lineIdx = c.line - 1;
|
|
279
332
|
if (lineIdx >= 0 && lineIdx < originalLines.length) {
|
|
@@ -324,9 +377,23 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
324
377
|
const indentMatch = targetLine.match(/^([ \t]*)/);
|
|
325
378
|
const indentation = indentMatch ? indentMatch[1] : '';
|
|
326
379
|
|
|
327
|
-
const commentLines = c.comment.split(/\r?\n/).map(line => {
|
|
328
|
-
|
|
380
|
+
const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
|
|
381
|
+
let trimmed = line.trimStart();
|
|
329
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
|
+
|
|
330
397
|
if (trimmed.startsWith('*') && !trimmed.startsWith('*/') && !trimmed.startsWith('/*')) {
|
|
331
398
|
return indentation + ' ' + trimmed;
|
|
332
399
|
}
|
|
@@ -348,12 +415,17 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
348
415
|
if (text === originalLine) {
|
|
349
416
|
return true;
|
|
350
417
|
}
|
|
351
|
-
if (mode === 'clean' && analysis) {
|
|
418
|
+
if ((mode === 'clean' || mode === 'prune') && analysis) {
|
|
352
419
|
const lineAnalysis = analysis[origIdx];
|
|
353
420
|
if (lineAnalysis && lineAnalysis.commentStartIndex !== -1 && !lineAnalysis.isPureComment) {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
}
|
|
357
429
|
}
|
|
358
430
|
}
|
|
359
431
|
}
|
|
@@ -376,6 +448,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
376
448
|
return annotated.map(line => line.text).join(lineEnding);
|
|
377
449
|
}
|
|
378
450
|
|
|
451
|
+
/** Main entry point for the CLI tool. [ds] */
|
|
379
452
|
/** Main entry point for the CLI tool. */
|
|
380
453
|
async function runCLI() {
|
|
381
454
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -395,6 +468,8 @@ Options:
|
|
|
395
468
|
--full Add detailed JSDoc/block comments and inline comments
|
|
396
469
|
--dry-run Preview comments without writing to file
|
|
397
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
|
|
398
473
|
--provider <name> Override AI provider (gemini, groq, openai, custom)
|
|
399
474
|
--model <name> Override AI model name
|
|
400
475
|
--api-key <key> Override API key for the provider
|
|
@@ -429,6 +504,7 @@ Options:
|
|
|
429
504
|
return;
|
|
430
505
|
}
|
|
431
506
|
|
|
507
|
+
// Helper to extract flag values from command line arguments [ds]
|
|
432
508
|
const getArgValue = (flag) => {
|
|
433
509
|
const index = args.indexOf(flag);
|
|
434
510
|
if (index !== -1 && index + 1 < args.length) {
|
|
@@ -461,6 +537,7 @@ Options:
|
|
|
461
537
|
if (args.includes('--light')) mode = 'light';
|
|
462
538
|
if (args.includes('--full')) mode = 'full';
|
|
463
539
|
if (args.includes('--clean')) mode = 'clean';
|
|
540
|
+
if (args.includes('--prune')) mode = 'prune';
|
|
464
541
|
const isDryRun = args.includes('--dry-run');
|
|
465
542
|
const isForce = args.includes('--force');
|
|
466
543
|
|
|
@@ -493,6 +570,7 @@ Options:
|
|
|
493
570
|
let successCount = 0;
|
|
494
571
|
let failCount = 0;
|
|
495
572
|
|
|
573
|
+
/** Recursively processes files or directories to apply AI-generated comments. [ds] */
|
|
496
574
|
/** Recursively processes files or directories to apply AI-generated comments. */
|
|
497
575
|
async function processPath(targetPath) {
|
|
498
576
|
const stats = fs.statSync(targetPath);
|
|
@@ -590,6 +668,7 @@ Options:
|
|
|
590
668
|
rl.close();
|
|
591
669
|
}
|
|
592
670
|
|
|
671
|
+
// Check if the script is run directly vs required as a module [ds]
|
|
593
672
|
// Check if the script is run directly vs required as a module
|
|
594
673
|
if (require.main === module) {
|
|
595
674
|
runCLI().catch(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