devsplain 2.0.0 → 2.0.1
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 +10 -10
- package/bin/cli.js +101 -39
- package/bin/setup-hook.js +22 -13
- package/lib/config.js +71 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# devsplain
|
|
2
|
-
An agent-agnostic CLI tool that adds JSDoc and inline comments using state-of-the-art LLMs while preserving non-comment source lines byte-for-byte through deterministic verification.
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
**devsplain never rewrites executable code**—it's a single-shot CLI that adds JSDoc and inline comments across 22 languages using LLMs, preserving non-comment source lines byte-for-byte through deterministic verification.
|
|
5
4
|
|
|
6
|
-
devsplain
|
|
7
|
-
If the original source cannot be reproduced exactly after comment insertion, the operation aborts.
|
|
5
|
+

|
|
8
6
|
|
|
9
|
-
Unlike interactive AI editors, `devsplain` is
|
|
7
|
+
Unlike interactive AI editors, `devsplain` is designed for batch documentation passes, CI pipelines, and git hook automation—no agent overhead, no per-file confirmation loops.
|
|
10
8
|
|
|
11
9
|
> [!TIP]
|
|
12
10
|
> **Built with devsplain**
|
|
@@ -16,7 +14,7 @@ Unlike interactive AI editors, `devsplain` is a single-shot CLI designed for bat
|
|
|
16
14
|
## Key Features
|
|
17
15
|
|
|
18
16
|
- **Deterministic Code Integrity Verification**: Uses an index-preserving splicing engine. Your non-comment source lines are guaranteed to remain byte-for-byte identical after comment insertion.
|
|
19
|
-
- **Multi-Language support**:
|
|
17
|
+
- **Multi-Language support**: Works across JavaScript, JSX, TypeScript, TSX, HTML, CSS, SCSS, Vue, Svelte, Python, Java, C, C++, C#, Go, Ruby, PHP, Rust, Swift, Kotlin, Dart, and Shell scripts.
|
|
20
18
|
- **Comment Preservation & Tagging**: AI-generated comments are tagged with `[ds]`. Your manually written comments are safe and will never be touched by the engine.
|
|
21
19
|
- **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.
|
|
22
20
|
- **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.
|
|
@@ -46,14 +44,14 @@ The engine tracks lexical state across template strings, single quotes, double q
|
|
|
46
44
|
|
|
47
45
|
### The `[ds]` AI Tag Guarantee
|
|
48
46
|
Every single comment generated by the LLM is forcibly prefixed with a `[ds]` tag (e.g., `// [ds] This function handles...`).
|
|
49
|
-
This guarantees that the local `devsplain` lexer can
|
|
47
|
+
This guarantees that the local `devsplain` lexer can lexically distinguish between your human-written manual comments and the AI-generated comments.
|
|
50
48
|
When you run the `--clean` command, the lexer looks specifically for the `[ds]` prefix and surgically removes only the AI-generated comments, safely preserving 100% of your manual documentation.
|
|
51
49
|
|
|
52
50
|
### Why Not AST Verification?
|
|
53
51
|
|
|
54
|
-
AST
|
|
52
|
+
AST parsers are language-specific. Supporting 22 languages would require 22 native parser dependencies, which defeats the zero-dependency architecture entirely.
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
The line-splicing + round-trip diff approach achieves equivalent guarantees without any of that bloat. This is not a compromise—it's the right answer for this tool's constraints:
|
|
57
55
|
|
|
58
56
|
1. Original source is loaded.
|
|
59
57
|
2. Comments are inserted.
|
|
@@ -178,7 +176,9 @@ SKIP_DEVSPLAIN=1 git commit -m "my commit message"
|
|
|
178
176
|
```
|
|
179
177
|
|
|
180
178
|
### Forcing or Skipping Auto-Prune
|
|
181
|
-
If
|
|
179
|
+
(If `autoPrune: true` is set in your `~/.devsplainrc` via the `--config` wizard, the hook scrubs all existing comments before regenerating them.)
|
|
180
|
+
|
|
181
|
+
If you want to change this behavior for a specific commit:
|
|
182
182
|
- To **force an overwrite** (destroying all human/AI comments before generating new ones) regardless of your config:
|
|
183
183
|
```bash
|
|
184
184
|
DS_OVER=1 git commit -m "overwrite docs"
|
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 Git
|
|
13
|
+
/** Checks if the Git repository is dirty [ds] */
|
|
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,47 +23,109 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Checks if a line is inside a string
|
|
26
|
+
/** Checks if a line of code is inside a string [ds] */
|
|
27
27
|
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
28
28
|
const isPython = ext.toLowerCase() === '.py';
|
|
29
|
+
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
29
30
|
let inBacktick = false;
|
|
30
31
|
let inTripleDouble = false;
|
|
31
32
|
let inTripleSingle = false;
|
|
32
33
|
let inSingle = false;
|
|
33
34
|
let inDouble = false;
|
|
34
|
-
|
|
35
|
+
let inBlockJS = false;
|
|
36
|
+
let inBlockHTML = false;
|
|
35
37
|
for (let i = 0; i < targetLineIndex; i++) {
|
|
36
38
|
const line = lines[i];
|
|
37
39
|
let j = 0;
|
|
38
40
|
while (j < line.length) {
|
|
41
|
+
if (inBlockJS) {
|
|
42
|
+
if (line.slice(j, j + 2) === '*/') {
|
|
43
|
+
inBlockJS = false;
|
|
44
|
+
j += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
j++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (inBlockHTML) {
|
|
51
|
+
if (line.slice(j, j + 3) === '-->') {
|
|
52
|
+
inBlockHTML = false;
|
|
53
|
+
j += 3;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
j++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Check if comment starts (skip processing quotes if we are entering a comment)
|
|
60
|
+
if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
|
|
61
|
+
if (isPython) {
|
|
62
|
+
if (line[j] === '#') {
|
|
63
|
+
break; // Ignore rest of line
|
|
64
|
+
}
|
|
65
|
+
} else if (isHTML) {
|
|
66
|
+
if (line.slice(j, j + 4) === '<!--') {
|
|
67
|
+
inBlockHTML = true;
|
|
68
|
+
// Check if current character is a backtick [ds]
|
|
69
|
+
j += 4;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
73
|
+
inBlockJS = true;
|
|
74
|
+
j += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (line.slice(j, j + 2) === '//') {
|
|
78
|
+
break; // Ignore rest of line
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (line.slice(j, j + 2) === '//') {
|
|
82
|
+
break; // Ignore rest of line
|
|
83
|
+
}
|
|
84
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
85
|
+
inBlockJS = true;
|
|
86
|
+
j += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
|
|
90
|
+
if (isShellOrRuby && line[j] === '#') {
|
|
91
|
+
break; // Ignore rest of line
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
39
95
|
if (isPython) {
|
|
40
|
-
if (!inTripleSingle) {
|
|
96
|
+
if (!inTripleSingle && !inSingle && !inDouble) {
|
|
41
97
|
if (line.slice(j, j + 3) === '"""') {
|
|
42
98
|
inTripleDouble = !inTripleDouble;
|
|
43
99
|
j += 3;
|
|
44
100
|
continue;
|
|
45
101
|
}
|
|
46
102
|
}
|
|
47
|
-
if (!inTripleDouble) {
|
|
103
|
+
if (!inTripleDouble && !inSingle && !inDouble) {
|
|
48
104
|
if (line.slice(j, j + 3) === "'''") {
|
|
49
105
|
inTripleSingle = !inTripleSingle;
|
|
50
106
|
j += 3;
|
|
51
107
|
continue;
|
|
52
108
|
}
|
|
53
109
|
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
escaped =
|
|
60
|
-
k
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
}
|
|
111
|
+
if (!inTripleSingle && !inTripleDouble) {
|
|
112
|
+
if (!isPython) {
|
|
113
|
+
if (!inSingle && !inDouble) {
|
|
114
|
+
if (line[j] === '`') {
|
|
115
|
+
let escaped = false;
|
|
116
|
+
let k = j - 1;
|
|
117
|
+
while (k >= 0 && line[k] === '\\') {
|
|
118
|
+
escaped = !escaped;
|
|
119
|
+
k--;
|
|
120
|
+
}
|
|
121
|
+
if (!escaped) {
|
|
122
|
+
inBacktick = !inBacktick;
|
|
123
|
+
}
|
|
64
124
|
}
|
|
65
125
|
}
|
|
126
|
+
}
|
|
66
127
|
if (!inBacktick) {
|
|
128
|
+
// Check if current character is a double quote [ds]
|
|
67
129
|
if (line[j] === '"' && !inSingle) {
|
|
68
130
|
let escaped = false;
|
|
69
131
|
let k = j - 1;
|
|
@@ -89,7 +151,8 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
89
151
|
}
|
|
90
152
|
j++;
|
|
91
153
|
}
|
|
92
|
-
|
|
154
|
+
const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
|
|
155
|
+
if (resetsAtLineEnd) {
|
|
93
156
|
inSingle = false;
|
|
94
157
|
inDouble = false;
|
|
95
158
|
}
|
|
@@ -97,7 +160,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
97
160
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
98
161
|
}
|
|
99
162
|
|
|
100
|
-
/** Analyzes comments in a list of lines [ds] */
|
|
163
|
+
/** Analyzes comments in a given list of lines [ds] */
|
|
101
164
|
function analyzeComments(lines, ext = '') {
|
|
102
165
|
const isPython = ext.toLowerCase() === '.py';
|
|
103
166
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -139,16 +202,6 @@ function analyzeComments(lines, ext = '') {
|
|
|
139
202
|
commentStartIndex = j;
|
|
140
203
|
break;
|
|
141
204
|
}
|
|
142
|
-
if (line.slice(j, j + 2) === '/*') {
|
|
143
|
-
commentStartIndex = j;
|
|
144
|
-
inBlockJS = true;
|
|
145
|
-
j += 2;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (line.slice(j, j + 2) === '//') {
|
|
149
|
-
commentStartIndex = j;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
205
|
} else if (isHTML) {
|
|
153
206
|
if (line.slice(j, j + 4) === '<!--') {
|
|
154
207
|
commentStartIndex = j;
|
|
@@ -177,30 +230,33 @@ function analyzeComments(lines, ext = '') {
|
|
|
177
230
|
j += 2;
|
|
178
231
|
continue;
|
|
179
232
|
}
|
|
180
|
-
|
|
233
|
+
const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
|
|
234
|
+
if (isShellOrRuby && line[j] === '#') {
|
|
181
235
|
commentStartIndex = j;
|
|
182
236
|
break;
|
|
183
237
|
}
|
|
184
238
|
}
|
|
185
239
|
}
|
|
186
240
|
if (isPython) {
|
|
187
|
-
if (!inTripleSingle) {
|
|
241
|
+
if (!inTripleSingle && !inSingle && !inDouble) {
|
|
188
242
|
if (line.slice(j, j + 3) === '"""') {
|
|
189
243
|
inTripleDouble = !inTripleDouble;
|
|
190
244
|
j += 3;
|
|
191
245
|
continue;
|
|
192
246
|
}
|
|
193
247
|
}
|
|
194
|
-
if (!inTripleDouble) {
|
|
248
|
+
if (!inTripleDouble && !inSingle && !inDouble) {
|
|
195
249
|
if (line.slice(j, j + 3) === "'''") {
|
|
196
250
|
inTripleSingle = !inTripleSingle;
|
|
197
251
|
j += 3;
|
|
198
252
|
continue;
|
|
199
253
|
}
|
|
200
254
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
255
|
+
}
|
|
256
|
+
if (!inTripleSingle && !inTripleDouble) {
|
|
257
|
+
if (!isPython) {
|
|
258
|
+
if (!inSingle && !inDouble) {
|
|
259
|
+
if (line[j] === '`') {
|
|
204
260
|
let escaped = false;
|
|
205
261
|
let k = j - 1;
|
|
206
262
|
while (k >= 0 && line[k] === '\\') {
|
|
@@ -212,6 +268,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
212
268
|
}
|
|
213
269
|
}
|
|
214
270
|
}
|
|
271
|
+
}
|
|
215
272
|
if (!inBacktick) {
|
|
216
273
|
if (line[j] === '"' && !inSingle) {
|
|
217
274
|
let escaped = false;
|
|
@@ -224,6 +281,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
224
281
|
inDouble = !inDouble;
|
|
225
282
|
}
|
|
226
283
|
}
|
|
284
|
+
// Check if current character is a single quote [ds]
|
|
227
285
|
else if (line[j] === "'" && !inDouble) {
|
|
228
286
|
let escaped = false;
|
|
229
287
|
let k = j - 1;
|
|
@@ -239,7 +297,8 @@ function analyzeComments(lines, ext = '') {
|
|
|
239
297
|
}
|
|
240
298
|
j++;
|
|
241
299
|
}
|
|
242
|
-
|
|
300
|
+
const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
|
|
301
|
+
if (resetsAtLineEnd) {
|
|
243
302
|
inSingle = false;
|
|
244
303
|
inDouble = false;
|
|
245
304
|
}
|
|
@@ -264,7 +323,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
264
323
|
return analysis;
|
|
265
324
|
}
|
|
266
325
|
|
|
267
|
-
/** Splices comments into
|
|
326
|
+
/** Splices comments into the given data [ds] */
|
|
268
327
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
269
328
|
const hasCRLF = data.includes('\r\n');
|
|
270
329
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -286,7 +345,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
286
345
|
let start = i;
|
|
287
346
|
let end = i;
|
|
288
347
|
while (end < originalLines.length && analysis[end].isInsideBlock) end++;
|
|
289
|
-
let blockStart = start - 1;
|
|
348
|
+
let blockStart = Math.max(0, start - 1);
|
|
290
349
|
let blockEnd = end - 1;
|
|
291
350
|
let hasDs = false;
|
|
292
351
|
for (let k = blockStart; k <= blockEnd; k++) {
|
|
@@ -313,6 +372,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
313
372
|
continue;
|
|
314
373
|
}
|
|
315
374
|
|
|
375
|
+
// Prune mode: remove all comments from the line [ds]
|
|
316
376
|
if (mode === 'prune') {
|
|
317
377
|
if (lineAnalysis.isPureComment) {
|
|
318
378
|
finalDeletions.add(lineNum);
|
|
@@ -387,6 +447,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
387
447
|
const indentation = indentMatch ? indentMatch[1] : '';
|
|
388
448
|
|
|
389
449
|
const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
|
|
450
|
+
// Add the comment to the list of comments [ds]
|
|
390
451
|
let trimmed = line.trimStart();
|
|
391
452
|
if (!trimmed) return '';
|
|
392
453
|
|
|
@@ -418,6 +479,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
418
479
|
const filteredText = filtered.map(line => line.text);
|
|
419
480
|
const filteredIndices = filtered.map(line => line.originalIndex);
|
|
420
481
|
|
|
482
|
+
// Check if the filtered text is equal to the original text [ds]
|
|
421
483
|
const textEqual = filteredText.every((text, idx) => {
|
|
422
484
|
const origIdx = filteredIndices[idx];
|
|
423
485
|
const originalLine = originalLines[origIdx];
|
|
@@ -456,7 +518,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
456
518
|
return annotated.map(line => line.text).join(lineEnding);
|
|
457
519
|
}
|
|
458
520
|
|
|
459
|
-
/** Runs the CLI
|
|
521
|
+
/** Runs the CLI interface [ds] */
|
|
460
522
|
async function runCLI() {
|
|
461
523
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
462
524
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -600,8 +662,8 @@ Options:
|
|
|
600
662
|
];
|
|
601
663
|
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
602
664
|
|
|
665
|
+
/** Checks if a given path is ignored [ds] */
|
|
603
666
|
function isPathIgnored(targetPath) {
|
|
604
|
-
// Checks if a path is ignored [ds]
|
|
605
667
|
const filename = path.basename(targetPath);
|
|
606
668
|
for (const pattern of allIgnored) {
|
|
607
669
|
const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
|
|
@@ -611,7 +673,7 @@ Options:
|
|
|
611
673
|
return false;
|
|
612
674
|
}
|
|
613
675
|
|
|
614
|
-
/** Processes a path
|
|
676
|
+
/** Processes a given path [ds] */
|
|
615
677
|
async function processPath(targetPath) {
|
|
616
678
|
const stats = fs.statSync(targetPath);
|
|
617
679
|
|
package/bin/setup-hook.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
|
|
6
|
-
/** Installs Git hooks
|
|
6
|
+
/** Installs Git hooks and configures commit settings [ds] */
|
|
7
7
|
async function installHooks() {
|
|
8
8
|
try {
|
|
9
9
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
@@ -13,9 +13,8 @@ async function installHooks() {
|
|
|
13
13
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Default commenting mode for Git commits [ds]
|
|
17
16
|
let modeChoice = '1';
|
|
18
|
-
// Check if
|
|
17
|
+
// Check if process is run in a TTY environment [ds]
|
|
19
18
|
if (process.stdout.isTTY) {
|
|
20
19
|
const rl = readline.createInterface({
|
|
21
20
|
input: process.stdin,
|
|
@@ -23,15 +22,25 @@ async function installHooks() {
|
|
|
23
22
|
});
|
|
24
23
|
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
25
24
|
|
|
25
|
+
// Prompt user to select the default commenting mode [ds]
|
|
26
26
|
console.log('\nSelect default commenting mode for Git commits:');
|
|
27
27
|
console.log('1. Balanced (mix of JSDoc and sparse inline comments)');
|
|
28
28
|
console.log('2. Light (JSDoc block comments above functions only)');
|
|
29
29
|
console.log('3. Full (aggressive inline commenting)');
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
// Validate user input for commenting mode [ds]
|
|
32
|
+
while (true) {
|
|
33
|
+
const answer = (await askQuestion('Select (1-3, default: 1): ')).trim();
|
|
34
|
+
if (answer === '' || ['1', '2', '3'].includes(answer)) {
|
|
35
|
+
modeChoice = answer || '1';
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
console.log('Invalid choice. Please select 1, 2, or 3.');
|
|
39
|
+
}
|
|
32
40
|
rl.close();
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
// Determine mode arguments based on user choice [ds]
|
|
35
44
|
let modeArgs = '';
|
|
36
45
|
if (modeChoice === '2') {
|
|
37
46
|
modeArgs = ' --light';
|
|
@@ -39,7 +48,7 @@ async function installHooks() {
|
|
|
39
48
|
modeArgs = ' --full';
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
//
|
|
51
|
+
// Define the pre-commit hook script [ds]
|
|
43
52
|
const preCommitHookPath = path.join(hooksDir, 'pre-commit');
|
|
44
53
|
const preCommitContent = `#!/bin/sh
|
|
45
54
|
# devsplain native pre-commit hook
|
|
@@ -48,16 +57,16 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
|
|
|
48
57
|
npm test || exit 1
|
|
49
58
|
fi
|
|
50
59
|
`;
|
|
51
|
-
// Write pre-commit hook
|
|
60
|
+
// Write the pre-commit hook to the Git hooks directory [ds]
|
|
52
61
|
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
53
62
|
try {
|
|
54
63
|
fs.chmodSync(preCommitHookPath, 0o755);
|
|
55
64
|
} catch (err) {}
|
|
56
65
|
|
|
57
|
-
//
|
|
66
|
+
// Define the post-commit script path [ds]
|
|
58
67
|
const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
|
|
59
68
|
|
|
60
|
-
//
|
|
69
|
+
// Define the post-commit hook script [ds]
|
|
61
70
|
const postCommitHookPath = path.join(hooksDir, 'post-commit');
|
|
62
71
|
const postCommitContent = `#!/bin/sh
|
|
63
72
|
# devsplain native post-commit hook
|
|
@@ -69,11 +78,11 @@ node "${postCommitScript}"${modeArgs} || exit 1
|
|
|
69
78
|
fs.chmodSync(postCommitHookPath, 0o755);
|
|
70
79
|
} catch (err) {}
|
|
71
80
|
|
|
81
|
+
// Inform the user about the successful installation of the post-commit hook [ds]
|
|
72
82
|
console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
|
|
73
83
|
|
|
74
|
-
//
|
|
84
|
+
// Check if a .devsplainignore file exists in the Git root directory [ds]
|
|
75
85
|
const ignorePath = path.join(gitRoot, '.devsplainignore');
|
|
76
|
-
// Check if .devsplainignore file exists [ds]
|
|
77
86
|
if (!fs.existsSync(ignorePath)) {
|
|
78
87
|
const defaultIgnore = `node_modules/
|
|
79
88
|
.git/
|
|
@@ -102,9 +111,9 @@ fixtures/
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
//
|
|
114
|
+
// Check if the script is run as the main module [ds]
|
|
106
115
|
if (require.main === module) {
|
|
107
116
|
installHooks();
|
|
108
117
|
}
|
|
109
|
-
// Export installHooks function [ds]
|
|
118
|
+
// Export the installHooks function for external use [ds]
|
|
110
119
|
module.exports = installHooks;
|
package/lib/config.js
CHANGED
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
const configPath = path.join(os.homedir(), '.devsplainrc');
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Prompts the user for a secret input, hiding the input from the console [ds] */
|
|
8
8
|
function askSecret(query) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
10
|
const rl = readline.createInterface({
|
|
@@ -14,7 +14,6 @@ function askSecret(query) {
|
|
|
14
14
|
process.stdout.write(query);
|
|
15
15
|
process.stdin.setRawMode(true);
|
|
16
16
|
let key = '';
|
|
17
|
-
// Handle key presses, including enter, backspace, and Ctrl+C [ds]
|
|
18
17
|
const onData = (char) => {
|
|
19
18
|
char = char.toString();
|
|
20
19
|
if (char === '\n' || char === '\r') {
|
|
@@ -34,7 +33,7 @@ function askSecret(query) {
|
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
/** Retrieves the configuration, either from environment variables or by prompting the user
|
|
36
|
+
/** Retrieves the configuration, either from environment variables or by prompting the user [ds] */
|
|
38
37
|
async function getConfig(forceWizard = false) {
|
|
39
38
|
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
40
39
|
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
@@ -48,7 +47,7 @@ async function getConfig(forceWizard = false) {
|
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// If
|
|
50
|
+
// If the configuration file does not exist or forceWizard is true, prompt the user to configure [ds]
|
|
52
51
|
if (!fs.existsSync(configPath) || forceWizard) {
|
|
53
52
|
let rl = readline.createInterface({
|
|
54
53
|
input: process.stdin,
|
|
@@ -59,12 +58,13 @@ async function getConfig(forceWizard = false) {
|
|
|
59
58
|
let config = null;
|
|
60
59
|
let confirmed = false;
|
|
61
60
|
|
|
61
|
+
// Continuously prompt the user until the configuration is confirmed [ds]
|
|
62
62
|
while (!confirmed) {
|
|
63
63
|
let baseUrl = "";
|
|
64
64
|
let model = "";
|
|
65
65
|
let provider = "";
|
|
66
66
|
|
|
67
|
-
// Display
|
|
67
|
+
// Display the list of available AI providers [ds]
|
|
68
68
|
console.log("\nWhich AI Provider Do You want to use?");
|
|
69
69
|
console.log("1. Groq (Free, Fast, Llama-3)");
|
|
70
70
|
console.log("2. Gemini (Free Tier)");
|
|
@@ -94,8 +94,16 @@ async function getConfig(forceWizard = false) {
|
|
|
94
94
|
model = customModel.trim() || 'gpt-4o';
|
|
95
95
|
} else if (choice === '4') {
|
|
96
96
|
provider = 'custom';
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
while (true) {
|
|
98
|
+
model = (await askQuestion("Model name (e.g., llama3): ")).trim();
|
|
99
|
+
if (model) break;
|
|
100
|
+
console.log("Model name cannot be empty.");
|
|
101
|
+
}
|
|
102
|
+
while (true) {
|
|
103
|
+
baseUrl = (await askQuestion("Base URL (e.g., http://localhost:11434): ")).trim();
|
|
104
|
+
if (baseUrl) break;
|
|
105
|
+
console.log("Base URL cannot be empty.");
|
|
106
|
+
}
|
|
99
107
|
} else if (choice === '5') {
|
|
100
108
|
provider = 'claude';
|
|
101
109
|
baseUrl = 'https://api.anthropic.com';
|
|
@@ -107,24 +115,46 @@ async function getConfig(forceWizard = false) {
|
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
// Prompt the user for an API key [ds]
|
|
110
119
|
let apiKey = '';
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
while (true) {
|
|
121
|
+
const promptMsg = provider === 'custom'
|
|
122
|
+
? "Paste your API key (leave blank for local models): "
|
|
123
|
+
: "Paste your API key: ";
|
|
124
|
+
|
|
125
|
+
if (process.stdin.isTTY) {
|
|
126
|
+
rl.close();
|
|
127
|
+
apiKey = await askSecret(promptMsg);
|
|
128
|
+
|
|
129
|
+
rl = readline.createInterface({
|
|
130
|
+
input: process.stdin,
|
|
131
|
+
output: process.stdout
|
|
132
|
+
});
|
|
133
|
+
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
134
|
+
} else {
|
|
135
|
+
apiKey = await askQuestion(promptMsg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
apiKey = apiKey.trim();
|
|
139
|
+
if (provider === 'custom' || apiKey) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
console.log(`API key is required for provider '${provider}'.`);
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
// Ask the user if they want to
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
// Ask the user if they want to enable auto-pruning of existing comments [ds]
|
|
146
|
+
let autoPrune = false;
|
|
147
|
+
while (true) {
|
|
148
|
+
const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
|
|
149
|
+
if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
|
|
150
|
+
autoPrune = false;
|
|
151
|
+
break;
|
|
152
|
+
} else if (pruneAns === 'y' || pruneAns === 'yes') {
|
|
153
|
+
autoPrune = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
157
|
+
}
|
|
128
158
|
|
|
129
159
|
// Display a summary of the configuration [ds]
|
|
130
160
|
console.log("\n--- Configuration Summary ---");
|
|
@@ -136,27 +166,31 @@ async function getConfig(forceWizard = false) {
|
|
|
136
166
|
console.log("-----------------------------\n");
|
|
137
167
|
|
|
138
168
|
// Confirm the configuration with the user [ds]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
while (true) {
|
|
170
|
+
const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
|
|
171
|
+
if (confirm === '' || confirm === 'y' || confirm === 'yes') {
|
|
172
|
+
config = {
|
|
173
|
+
provider,
|
|
174
|
+
apiKey,
|
|
175
|
+
model,
|
|
176
|
+
baseUrl,
|
|
177
|
+
autoPrune
|
|
178
|
+
};
|
|
179
|
+
confirmed = true;
|
|
180
|
+
break;
|
|
181
|
+
} else if (confirm === 'n' || confirm === 'no') {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
rl.close();
|
|
155
189
|
|
|
156
|
-
// Write the configuration to
|
|
190
|
+
// Write the configuration to the config file [ds]
|
|
157
191
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
158
192
|
try {
|
|
159
|
-
// Set permissions
|
|
193
|
+
// Set the permissions of the config file to prevent other users from reading it [ds]
|
|
160
194
|
if (process.platform !== 'win32') {
|
|
161
195
|
fs.chmodSync(configPath, 0o600);
|
|
162
196
|
}
|
|
@@ -170,4 +204,4 @@ async function getConfig(forceWizard = false) {
|
|
|
170
204
|
}
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
module.exports = { getConfig };
|
|
207
|
+
module.exports = { getConfig };
|
package/package.json
CHANGED