devsplain 2.0.0 → 2.1.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 +10 -10
- package/bin/cli.js +240 -40
- package/bin/setup-hook.js +22 -13
- package/lib/config.js +73 -39
- package/lib/llm.js +8 -32
- 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 has uncommitted changes [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,46 +23,178 @@ function isGitDirty() {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Checks if a line is inside a string literal [ds] */
|
|
26
|
+
/** Checks if a line of code is inside a string literal [ds] */
|
|
27
27
|
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
28
28
|
const isPython = ext.toLowerCase() === '.py';
|
|
29
|
+
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
30
|
+
const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
|
|
31
|
+
const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
|
|
32
|
+
const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
|
|
29
33
|
let inBacktick = false;
|
|
30
34
|
let inTripleDouble = false;
|
|
31
35
|
let inTripleSingle = false;
|
|
32
36
|
let inSingle = false;
|
|
33
37
|
let inDouble = false;
|
|
34
|
-
|
|
38
|
+
let inBlockJS = false;
|
|
39
|
+
let inBlockHTML = false;
|
|
40
|
+
let blockDepthJS = 0;
|
|
41
|
+
let inCppRawString = false;
|
|
42
|
+
let cppRawDelimiter = '';
|
|
43
|
+
let inRegex = false;
|
|
35
44
|
for (let i = 0; i < targetLineIndex; i++) {
|
|
36
45
|
const line = lines[i];
|
|
37
46
|
let j = 0;
|
|
38
47
|
while (j < line.length) {
|
|
48
|
+
if (inBlockJS) {
|
|
49
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
50
|
+
if (isRustOrSwift) blockDepthJS++;
|
|
51
|
+
j += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (line.slice(j, j + 2) === '*/') {
|
|
55
|
+
if (isRustOrSwift && blockDepthJS > 1) {
|
|
56
|
+
blockDepthJS--;
|
|
57
|
+
} else {
|
|
58
|
+
inBlockJS = false;
|
|
59
|
+
blockDepthJS = 0;
|
|
60
|
+
}
|
|
61
|
+
j += 2;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
j++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (inCppRawString) {
|
|
68
|
+
if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
|
|
69
|
+
inCppRawString = false;
|
|
70
|
+
j += 2 + cppRawDelimiter.length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
j++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (inRegex) {
|
|
77
|
+
let escaped = false;
|
|
78
|
+
let k = j - 1;
|
|
79
|
+
while (k >= 0 && line[k] === '\\') {
|
|
80
|
+
escaped = !escaped;
|
|
81
|
+
k--;
|
|
82
|
+
}
|
|
83
|
+
if (line[j] === '/' && !escaped) {
|
|
84
|
+
inRegex = false;
|
|
85
|
+
}
|
|
86
|
+
j++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (inBlockHTML) {
|
|
90
|
+
if (line.slice(j, j + 3) === '-->') {
|
|
91
|
+
inBlockHTML = false;
|
|
92
|
+
j += 3;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
j++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Check if comment starts (skip processing quotes if we are entering a comment)
|
|
99
|
+
if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
|
|
100
|
+
if (isPython) {
|
|
101
|
+
if (line[j] === '#') {
|
|
102
|
+
break; // Ignore rest of line
|
|
103
|
+
}
|
|
104
|
+
} else if (isHTML) {
|
|
105
|
+
if (line.slice(j, j + 4) === '<!--') {
|
|
106
|
+
inBlockHTML = true;
|
|
107
|
+
j += 4;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
111
|
+
inBlockJS = true;
|
|
112
|
+
j += 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (line.slice(j, j + 2) === '//') {
|
|
116
|
+
break; // Ignore rest of line
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
if (line.slice(j, j + 2) === '//') {
|
|
120
|
+
break; // Ignore rest of line
|
|
121
|
+
}
|
|
122
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
123
|
+
inBlockJS = true;
|
|
124
|
+
blockDepthJS = 1;
|
|
125
|
+
j += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
|
|
129
|
+
if (isShellOrRuby && line[j] === '#') {
|
|
130
|
+
break; // Ignore rest of line
|
|
131
|
+
}
|
|
132
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
133
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
134
|
+
if (match) {
|
|
135
|
+
cppRawDelimiter = match[1];
|
|
136
|
+
inCppRawString = true;
|
|
137
|
+
j += match[0].length;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (isJS && line[j] === '/') {
|
|
142
|
+
let k = j - 1;
|
|
143
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
144
|
+
let isRegex = false;
|
|
145
|
+
if (k < 0) {
|
|
146
|
+
isRegex = true;
|
|
147
|
+
} else {
|
|
148
|
+
const prevChar = line[k];
|
|
149
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
150
|
+
isRegex = true;
|
|
151
|
+
} else {
|
|
152
|
+
const prefix = line.slice(0, k + 1);
|
|
153
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
154
|
+
isRegex = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (isRegex) {
|
|
159
|
+
inRegex = true;
|
|
160
|
+
j++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
39
166
|
if (isPython) {
|
|
40
|
-
if (!inTripleSingle) {
|
|
167
|
+
if (!inTripleSingle && !inSingle && !inDouble) {
|
|
41
168
|
if (line.slice(j, j + 3) === '"""') {
|
|
42
169
|
inTripleDouble = !inTripleDouble;
|
|
43
170
|
j += 3;
|
|
44
171
|
continue;
|
|
45
172
|
}
|
|
46
173
|
}
|
|
47
|
-
if (!inTripleDouble) {
|
|
174
|
+
if (!inTripleDouble && !inSingle && !inDouble) {
|
|
48
175
|
if (line.slice(j, j + 3) === "'''") {
|
|
49
176
|
inTripleSingle = !inTripleSingle;
|
|
50
177
|
j += 3;
|
|
51
178
|
continue;
|
|
52
179
|
}
|
|
53
180
|
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
escaped =
|
|
60
|
-
k
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
181
|
+
}
|
|
182
|
+
if (!inTripleSingle && !inTripleDouble) {
|
|
183
|
+
if (!isPython) {
|
|
184
|
+
if (!inSingle && !inDouble) {
|
|
185
|
+
if (line[j] === '`') {
|
|
186
|
+
let escaped = false;
|
|
187
|
+
let k = j - 1;
|
|
188
|
+
while (k >= 0 && line[k] === '\\') {
|
|
189
|
+
escaped = !escaped;
|
|
190
|
+
k--;
|
|
191
|
+
}
|
|
192
|
+
if (!escaped) {
|
|
193
|
+
inBacktick = !inBacktick;
|
|
194
|
+
}
|
|
64
195
|
}
|
|
65
196
|
}
|
|
197
|
+
}
|
|
66
198
|
if (!inBacktick) {
|
|
67
199
|
if (line[j] === '"' && !inSingle) {
|
|
68
200
|
let escaped = false;
|
|
@@ -89,18 +221,22 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
89
221
|
}
|
|
90
222
|
j++;
|
|
91
223
|
}
|
|
92
|
-
|
|
224
|
+
const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
|
|
225
|
+
if (resetsAtLineEnd) {
|
|
93
226
|
inSingle = false;
|
|
94
227
|
inDouble = false;
|
|
95
228
|
}
|
|
96
229
|
}
|
|
97
|
-
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
230
|
+
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
|
|
98
231
|
}
|
|
99
232
|
|
|
100
|
-
/** Analyzes comments in a
|
|
233
|
+
/** Analyzes the comments in a given set of code lines [ds] */
|
|
101
234
|
function analyzeComments(lines, ext = '') {
|
|
102
235
|
const isPython = ext.toLowerCase() === '.py';
|
|
103
236
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
237
|
+
const isRustOrSwift = ['.rs', '.swift'].includes(ext.toLowerCase());
|
|
238
|
+
const isCpp = ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'].includes(ext.toLowerCase());
|
|
239
|
+
const isJS = ['.js', '.jsx', '.ts', '.tsx'].includes(ext.toLowerCase());
|
|
104
240
|
const analysis = [];
|
|
105
241
|
let inBacktick = false;
|
|
106
242
|
let inTripleDouble = false;
|
|
@@ -109,6 +245,10 @@ function analyzeComments(lines, ext = '') {
|
|
|
109
245
|
let inDouble = false;
|
|
110
246
|
let inBlockJS = false;
|
|
111
247
|
let inBlockHTML = false;
|
|
248
|
+
let blockDepthJS = 0;
|
|
249
|
+
let inCppRawString = false;
|
|
250
|
+
let cppRawDelimiter = '';
|
|
251
|
+
let inRegex = false;
|
|
112
252
|
for (let i = 0; i < lines.length; i++) {
|
|
113
253
|
const line = lines[i];
|
|
114
254
|
let commentStartIndex = -1;
|
|
@@ -116,14 +256,46 @@ function analyzeComments(lines, ext = '') {
|
|
|
116
256
|
let j = 0;
|
|
117
257
|
while (j < line.length) {
|
|
118
258
|
if (inBlockJS) {
|
|
259
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
260
|
+
if (isRustOrSwift) blockDepthJS++;
|
|
261
|
+
j += 2;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
119
264
|
if (line.slice(j, j + 2) === '*/') {
|
|
120
|
-
|
|
265
|
+
if (isRustOrSwift && blockDepthJS > 1) {
|
|
266
|
+
blockDepthJS--;
|
|
267
|
+
} else {
|
|
268
|
+
inBlockJS = false;
|
|
269
|
+
blockDepthJS = 0;
|
|
270
|
+
}
|
|
121
271
|
j += 2;
|
|
122
272
|
continue;
|
|
123
273
|
}
|
|
124
274
|
j++;
|
|
125
275
|
continue;
|
|
126
276
|
}
|
|
277
|
+
if (inCppRawString) {
|
|
278
|
+
if (line.slice(j, j + 2 + cppRawDelimiter.length) === ')' + cppRawDelimiter + '"') {
|
|
279
|
+
inCppRawString = false;
|
|
280
|
+
j += 2 + cppRawDelimiter.length;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
j++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (inRegex) {
|
|
287
|
+
let escaped = false;
|
|
288
|
+
let k = j - 1;
|
|
289
|
+
while (k >= 0 && line[k] === '\\') {
|
|
290
|
+
escaped = !escaped;
|
|
291
|
+
k--;
|
|
292
|
+
}
|
|
293
|
+
if (line[j] === '/' && !escaped) {
|
|
294
|
+
inRegex = false;
|
|
295
|
+
}
|
|
296
|
+
j++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
127
299
|
if (inBlockHTML) {
|
|
128
300
|
if (line.slice(j, j + 3) === '-->') {
|
|
129
301
|
inBlockHTML = false;
|
|
@@ -139,16 +311,6 @@ function analyzeComments(lines, ext = '') {
|
|
|
139
311
|
commentStartIndex = j;
|
|
140
312
|
break;
|
|
141
313
|
}
|
|
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
314
|
} else if (isHTML) {
|
|
153
315
|
if (line.slice(j, j + 4) === '<!--') {
|
|
154
316
|
commentStartIndex = j;
|
|
@@ -174,33 +336,69 @@ function analyzeComments(lines, ext = '') {
|
|
|
174
336
|
if (line.slice(j, j + 2) === '/*') {
|
|
175
337
|
commentStartIndex = j;
|
|
176
338
|
inBlockJS = true;
|
|
339
|
+
blockDepthJS = 1;
|
|
177
340
|
j += 2;
|
|
178
341
|
continue;
|
|
179
342
|
}
|
|
180
|
-
|
|
343
|
+
const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
|
|
344
|
+
if (isShellOrRuby && line[j] === '#') {
|
|
181
345
|
commentStartIndex = j;
|
|
182
346
|
break;
|
|
183
347
|
}
|
|
348
|
+
if (isCpp && line[j] === 'R' && line[j+1] === '"') {
|
|
349
|
+
const match = line.slice(j).match(/^R"([^()\\\s]{0,16})\(/);
|
|
350
|
+
if (match) {
|
|
351
|
+
cppRawDelimiter = match[1];
|
|
352
|
+
inCppRawString = true;
|
|
353
|
+
j += match[0].length;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (isJS && line[j] === '/') {
|
|
358
|
+
let k = j - 1;
|
|
359
|
+
while (k >= 0 && /\s/.test(line[k])) k--;
|
|
360
|
+
let isRegex = false;
|
|
361
|
+
if (k < 0) {
|
|
362
|
+
isRegex = true;
|
|
363
|
+
} else {
|
|
364
|
+
const prevChar = line[k];
|
|
365
|
+
if (/[=({\[:,;!+*&|?<>-]/.test(prevChar)) {
|
|
366
|
+
isRegex = true;
|
|
367
|
+
} else {
|
|
368
|
+
const prefix = line.slice(0, k + 1);
|
|
369
|
+
if (/(?:return|typeof|yield|await|throw)\s*$/.test(prefix)) {
|
|
370
|
+
isRegex = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (isRegex) {
|
|
375
|
+
inRegex = true;
|
|
376
|
+
j++;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
184
380
|
}
|
|
185
381
|
}
|
|
186
382
|
if (isPython) {
|
|
187
|
-
if (!inTripleSingle) {
|
|
383
|
+
if (!inTripleSingle && !inSingle && !inDouble) {
|
|
188
384
|
if (line.slice(j, j + 3) === '"""') {
|
|
189
385
|
inTripleDouble = !inTripleDouble;
|
|
190
386
|
j += 3;
|
|
191
387
|
continue;
|
|
192
388
|
}
|
|
193
389
|
}
|
|
194
|
-
if (!inTripleDouble) {
|
|
390
|
+
if (!inTripleDouble && !inSingle && !inDouble) {
|
|
195
391
|
if (line.slice(j, j + 3) === "'''") {
|
|
196
392
|
inTripleSingle = !inTripleSingle;
|
|
197
393
|
j += 3;
|
|
198
394
|
continue;
|
|
199
395
|
}
|
|
200
396
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
397
|
+
}
|
|
398
|
+
if (!inTripleSingle && !inTripleDouble) {
|
|
399
|
+
if (!isPython) {
|
|
400
|
+
if (!inSingle && !inDouble) {
|
|
401
|
+
if (line[j] === '`') {
|
|
204
402
|
let escaped = false;
|
|
205
403
|
let k = j - 1;
|
|
206
404
|
while (k >= 0 && line[k] === '\\') {
|
|
@@ -212,6 +410,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
212
410
|
}
|
|
213
411
|
}
|
|
214
412
|
}
|
|
413
|
+
}
|
|
215
414
|
if (!inBacktick) {
|
|
216
415
|
if (line[j] === '"' && !inSingle) {
|
|
217
416
|
let escaped = false;
|
|
@@ -239,7 +438,8 @@ function analyzeComments(lines, ext = '') {
|
|
|
239
438
|
}
|
|
240
439
|
j++;
|
|
241
440
|
}
|
|
242
|
-
|
|
441
|
+
const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
|
|
442
|
+
if (resetsAtLineEnd) {
|
|
243
443
|
inSingle = false;
|
|
244
444
|
inDouble = false;
|
|
245
445
|
}
|
|
@@ -264,7 +464,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
264
464
|
return analysis;
|
|
265
465
|
}
|
|
266
466
|
|
|
267
|
-
/** Splices comments into
|
|
467
|
+
/** Splices comments into the provided code data [ds] */
|
|
268
468
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
269
469
|
const hasCRLF = data.includes('\r\n');
|
|
270
470
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -286,7 +486,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
286
486
|
let start = i;
|
|
287
487
|
let end = i;
|
|
288
488
|
while (end < originalLines.length && analysis[end].isInsideBlock) end++;
|
|
289
|
-
let blockStart = start - 1;
|
|
489
|
+
let blockStart = Math.max(0, start - 1);
|
|
290
490
|
let blockEnd = end - 1;
|
|
291
491
|
let hasDs = false;
|
|
292
492
|
for (let k = blockStart; k <= blockEnd; k++) {
|
|
@@ -600,8 +800,8 @@ Options:
|
|
|
600
800
|
];
|
|
601
801
|
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
602
802
|
|
|
803
|
+
/** Checks if a path is ignored based on the ignore patterns [ds] */
|
|
603
804
|
function isPathIgnored(targetPath) {
|
|
604
|
-
// Checks if a path is ignored [ds]
|
|
605
805
|
const filename = path.basename(targetPath);
|
|
606
806
|
for (const pattern of allIgnored) {
|
|
607
807
|
const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
|
|
@@ -611,7 +811,7 @@ Options:
|
|
|
611
811
|
return false;
|
|
612
812
|
}
|
|
613
813
|
|
|
614
|
-
/** Processes a path,
|
|
814
|
+
/** Processes a given path, which can be a file or directory [ds] */
|
|
615
815
|
async function processPath(targetPath) {
|
|
616
816
|
const stats = fs.statSync(targetPath);
|
|
617
817
|
|
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
|
-
/** Asks the user for a secret input
|
|
7
|
+
/** Asks the user for a secret input (e.g., an API key) without echoing the input to 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
|
-
/**
|
|
36
|
+
/** Attempts to fetch the configuration settings, 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';
|
|
@@ -47,8 +46,8 @@ async function getConfig(forceWizard = false) {
|
|
|
47
46
|
baseUrl
|
|
48
47
|
};
|
|
49
48
|
}
|
|
49
|
+
// If the configuration file doesn't exist, or if we're forcing the wizard, let's prompt the user for configuration. [ds]
|
|
50
50
|
|
|
51
|
-
// If no configuration file exists or the forceWizard flag is set, prompt the user to set up a configuration [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
|
+
// Keep prompting the user until they confirm their configuration settings. [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 to the user. [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,26 +115,48 @@ async function getConfig(forceWizard = false) {
|
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
// Get the API key from the user. [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 human/AI 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
|
-
// Display a summary of the configuration [ds]
|
|
159
|
+
// Display a summary of the user's configuration settings. [ds]
|
|
130
160
|
console.log("\n--- Configuration Summary ---");
|
|
131
161
|
console.log(`Provider: ${provider}`);
|
|
132
162
|
console.log(`Model: ${model}`);
|
|
@@ -135,28 +165,31 @@ async function getConfig(forceWizard = false) {
|
|
|
135
165
|
console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
|
|
136
166
|
console.log("-----------------------------\n");
|
|
137
167
|
|
|
138
|
-
// Confirm the configuration with the user [ds]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
// Confirm the configuration settings with the user. [ds]
|
|
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 settings to the config file. [ds]
|
|
157
191
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
158
192
|
try {
|
|
159
|
-
// Set permissions on the configuration file to prevent other users from reading it [ds]
|
|
160
193
|
if (process.platform !== 'win32') {
|
|
161
194
|
fs.chmodSync(configPath, 0o600);
|
|
162
195
|
}
|
|
@@ -170,4 +203,5 @@ async function getConfig(forceWizard = false) {
|
|
|
170
203
|
}
|
|
171
204
|
}
|
|
172
205
|
|
|
173
|
-
|
|
206
|
+
// Export the getConfig function for use in other modules. [ds]
|
|
207
|
+
module.exports = { getConfig };
|
package/lib/llm.js
CHANGED
|
@@ -41,37 +41,14 @@ async function getComments(code, language, config, mode = 'default') {
|
|
|
41
41
|
const lines = code.split(/\r?\n/);
|
|
42
42
|
const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
|
|
43
43
|
|
|
44
|
-
let
|
|
45
|
-
if (mode === '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
CRITICAL RULES:
|
|
51
|
-
1. You MUST respond with ONLY a raw, valid JSON array of objects. NO markdown formatting, NO backticks, NO explanations, NO text before or after the JSON.
|
|
52
|
-
2. Each object must have exactly two properties: "line" (the integer line number of the comment line to delete) and "action" (which must be the string "delete").
|
|
53
|
-
3. Do NOT include the original code in your response.
|
|
54
|
-
4. If no comments are found, return an empty array: [].
|
|
55
|
-
5. For block or JSDoc comments (e.g., starting with /* and ending with */), you MUST identify and return the line numbers of ALL lines in that block, including the opening /*, all intermediate lines, and the closing */. Do NOT leave trailing comment delimiters behind.
|
|
56
|
-
|
|
57
|
-
Example Output:
|
|
58
|
-
[
|
|
59
|
-
{ "line": 4, "action": "delete" },
|
|
60
|
-
{ "line": 5, "action": "delete" }
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
Here is the source code:
|
|
64
|
-
${numberedCode}
|
|
65
|
-
`.trim();
|
|
66
|
-
} else {
|
|
67
|
-
let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
|
|
68
|
-
if (mode === 'light') {
|
|
69
|
-
instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
|
|
70
|
-
} else if (mode === 'full') {
|
|
71
|
-
instruction = "Provide highly detailed JSDoc/docstrings above functions, and exhaustive step-by-step inline comments (using standard comment syntax like // or #) explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.";
|
|
72
|
-
}
|
|
44
|
+
let instruction = "Provide JSDoc/docstrings block comments above functions and sparse inline comments for complex logic.";
|
|
45
|
+
if (mode === 'light') {
|
|
46
|
+
instruction = "Provide ONLY JSDoc/docstrings above functions. Keep it minimal.";
|
|
47
|
+
} else if (mode === 'full') {
|
|
48
|
+
instruction = "Provide highly detailed JSDoc/docstrings above functions, and exhaustive step-by-step inline comments (using standard comment syntax like // or #) explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.";
|
|
49
|
+
}
|
|
73
50
|
|
|
74
|
-
|
|
51
|
+
let prompt = `
|
|
75
52
|
You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
|
|
76
53
|
${instruction}
|
|
77
54
|
|
|
@@ -89,8 +66,7 @@ Example Output:
|
|
|
89
66
|
|
|
90
67
|
Here is the source code:
|
|
91
68
|
${numberedCode}
|
|
92
|
-
|
|
93
|
-
}
|
|
69
|
+
`.trim();
|
|
94
70
|
|
|
95
71
|
// Initialize text response [ds]
|
|
96
72
|
let textResponse = "";
|
package/package.json
CHANGED