devsplain 1.8.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 +44 -17
- package/bin/cli.js +141 -56
- package/bin/post-commit.js +24 -18
- package/bin/setup-hook.js +49 -17
- package/lib/config.js +111 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# devsplain
|
|
2
|
-
|
|
2
|
+
|
|
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.
|
|
3
4
|
|
|
4
5
|

|
|
5
6
|
|
|
6
|
-
devsplain
|
|
7
|
-
|
|
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.
|
|
8
|
+
|
|
9
|
+
> [!TIP]
|
|
10
|
+
> **Built with devsplain**
|
|
11
|
+
> This entire repository is heavily "dogfooded". Every single AI-generated `[ds]` comment you see in the source code of this tool was automatically generated and committed by `devsplain` itself using its native git hooks!
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
## Key Features
|
|
11
15
|
|
|
12
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.
|
|
13
|
-
- **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.
|
|
14
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.
|
|
15
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.
|
|
16
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.
|
|
@@ -40,14 +44,14 @@ The engine tracks lexical state across template strings, single quotes, double q
|
|
|
40
44
|
|
|
41
45
|
### The `[ds]` AI Tag Guarantee
|
|
42
46
|
Every single comment generated by the LLM is forcibly prefixed with a `[ds]` tag (e.g., `// [ds] This function handles...`).
|
|
43
|
-
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.
|
|
44
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.
|
|
45
49
|
|
|
46
50
|
### Why Not AST Verification?
|
|
47
51
|
|
|
48
|
-
AST
|
|
52
|
+
AST parsers are language-specific. Supporting 22 languages would require 22 native parser dependencies, which defeats the zero-dependency architecture entirely.
|
|
49
53
|
|
|
50
|
-
|
|
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:
|
|
51
55
|
|
|
52
56
|
1. Original source is loaded.
|
|
53
57
|
2. Comments are inserted.
|
|
@@ -81,10 +85,10 @@ To force re-run the configuration wizard at any time, execute:
|
|
|
81
85
|
devsplain --config
|
|
82
86
|
```
|
|
83
87
|
|
|
84
|
-
Your settings are stored securely in `~/.devsplainrc` (configured with `chmod 600` on POSIX systems to restrict read access).
|
|
88
|
+
Your settings are stored securely in `~/.devsplainrc` (configured with `chmod 600` on POSIX systems to restrict read access, and keystrokes are masked during input).
|
|
85
89
|
|
|
86
|
-
> [!
|
|
87
|
-
> **
|
|
90
|
+
> [!CAUTION]
|
|
91
|
+
> **Security Note:** Prefer configuring the `DEVSPLAIN_API_KEY` environment variable over using the `--api-key` CLI flag for recurring use. CLI flags may be exposed in your shell history (`~/.bash_history`) and process lists.
|
|
88
92
|
|
|
89
93
|
---
|
|
90
94
|
|
|
@@ -133,13 +137,10 @@ devsplain lib/ --prune
|
|
|
133
137
|
devsplain src/utils.ts --provider gemini --model gemini-2.0-flash --api-key YOUR_KEY
|
|
134
138
|
```
|
|
135
139
|
|
|
136
|
-
> [!
|
|
137
|
-
> **Directory Traversal
|
|
138
|
-
>
|
|
139
|
-
>
|
|
140
|
-
> To prevent this, it is highly recommended to either:
|
|
141
|
-
> 1. Use the **Automated Git Hooks** to comment only on files modified in your commits.
|
|
142
|
-
> 2. Pass specific files or selective subfolders manually (e.g., `devsplain src/utils.ts`) instead of targeting broad directories.
|
|
140
|
+
> [!TIP]
|
|
141
|
+
> **Directory Traversal Protection**
|
|
142
|
+
> When you run `devsplain` on a directory, it automatically ignores common build/dependency folders (`node_modules`, `.git`, `dist`, etc.).
|
|
143
|
+
> To ignore custom directories, simply create a `.devsplainignore` file in your project root using the standard `.gitignore` syntax. (A default `.devsplainignore` is automatically generated when you run `--setup-hook`).
|
|
143
144
|
|
|
144
145
|
---
|
|
145
146
|
|
|
@@ -168,6 +169,25 @@ Run the hook setup command inside your Git repository:
|
|
|
168
169
|
devsplain --setup-hook
|
|
169
170
|
```
|
|
170
171
|
|
|
172
|
+
### Bypassing the Hook (Manual Override)
|
|
173
|
+
If you ever want to commit code without triggering the AI (for example, if you just ran `devsplain index.js --full` manually and want to freeze those specific comments without the background hook overwriting them), you can bypass the hook entirely:
|
|
174
|
+
```bash
|
|
175
|
+
SKIP_DEVSPLAIN=1 git commit -m "my commit message"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Forcing or Skipping Auto-Prune
|
|
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
|
+
- To **force an overwrite** (destroying all human/AI comments before generating new ones) regardless of your config:
|
|
183
|
+
```bash
|
|
184
|
+
DS_OVER=1 git commit -m "overwrite docs"
|
|
185
|
+
```
|
|
186
|
+
- To **force preservation** of existing comments (overriding a global prune preference):
|
|
187
|
+
```bash
|
|
188
|
+
DS_KEEP=1 git commit -m "keep docs"
|
|
189
|
+
```
|
|
190
|
+
|
|
171
191
|
### Pipeline Architecture
|
|
172
192
|
1. **Pre-commit Hook**: Runs your project test suite (`npm test`). If the tests fail, the commit is blocked.
|
|
173
193
|
2. **Post-commit Hook**:
|
|
@@ -179,6 +199,13 @@ devsplain --setup-hook
|
|
|
179
199
|
|
|
180
200
|
---
|
|
181
201
|
|
|
202
|
+
## Known Limitations & Contributing
|
|
203
|
+
|
|
204
|
+
> [!NOTE]
|
|
205
|
+
> **API Testing Notice:** `devsplain` natively supports OpenAI, Anthropic, Ollama, Groq, and Gemini endpoints, but the E2E test suite has currently only been aggressively verified against the **Groq** and **Gemini** APIs. If you encounter any unexpected parsing issues or edge-case errors with other providers, please open an issue or submit a PR!
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
182
209
|
## License
|
|
183
210
|
|
|
184
211
|
This project is licensed under the MIT License.
|
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
|
|
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
|
|
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
|
|
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());
|
|
@@ -133,23 +196,12 @@ function analyzeComments(lines, ext = '') {
|
|
|
133
196
|
j++;
|
|
134
197
|
continue;
|
|
135
198
|
}
|
|
136
|
-
// Check for comment start index in non-Python files [ds]
|
|
137
199
|
if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
|
|
138
200
|
if (isPython) {
|
|
139
201
|
if (line[j] === '#') {
|
|
140
202
|
commentStartIndex = j;
|
|
141
203
|
break;
|
|
142
204
|
}
|
|
143
|
-
if (line.slice(j, j + 2) === '/*') {
|
|
144
|
-
commentStartIndex = j;
|
|
145
|
-
inBlockJS = true;
|
|
146
|
-
j += 2;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (line.slice(j, j + 2) === '//') {
|
|
150
|
-
commentStartIndex = j;
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
205
|
} else if (isHTML) {
|
|
154
206
|
if (line.slice(j, j + 4) === '<!--') {
|
|
155
207
|
commentStartIndex = j;
|
|
@@ -178,31 +230,33 @@ function analyzeComments(lines, ext = '') {
|
|
|
178
230
|
j += 2;
|
|
179
231
|
continue;
|
|
180
232
|
}
|
|
181
|
-
|
|
233
|
+
const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
|
|
234
|
+
if (isShellOrRuby && line[j] === '#') {
|
|
182
235
|
commentStartIndex = j;
|
|
183
236
|
break;
|
|
184
237
|
}
|
|
185
238
|
}
|
|
186
239
|
}
|
|
187
240
|
if (isPython) {
|
|
188
|
-
if (!inTripleSingle) {
|
|
241
|
+
if (!inTripleSingle && !inSingle && !inDouble) {
|
|
189
242
|
if (line.slice(j, j + 3) === '"""') {
|
|
190
243
|
inTripleDouble = !inTripleDouble;
|
|
191
244
|
j += 3;
|
|
192
245
|
continue;
|
|
193
246
|
}
|
|
194
247
|
}
|
|
195
|
-
if (!inTripleDouble) {
|
|
248
|
+
if (!inTripleDouble && !inSingle && !inDouble) {
|
|
196
249
|
if (line.slice(j, j + 3) === "'''") {
|
|
197
250
|
inTripleSingle = !inTripleSingle;
|
|
198
251
|
j += 3;
|
|
199
252
|
continue;
|
|
200
253
|
}
|
|
201
254
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!
|
|
205
|
-
if (
|
|
255
|
+
}
|
|
256
|
+
if (!inTripleSingle && !inTripleDouble) {
|
|
257
|
+
if (!isPython) {
|
|
258
|
+
if (!inSingle && !inDouble) {
|
|
259
|
+
if (line[j] === '`') {
|
|
206
260
|
let escaped = false;
|
|
207
261
|
let k = j - 1;
|
|
208
262
|
while (k >= 0 && line[k] === '\\') {
|
|
@@ -214,6 +268,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
214
268
|
}
|
|
215
269
|
}
|
|
216
270
|
}
|
|
271
|
+
}
|
|
217
272
|
if (!inBacktick) {
|
|
218
273
|
if (line[j] === '"' && !inSingle) {
|
|
219
274
|
let escaped = false;
|
|
@@ -226,6 +281,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
226
281
|
inDouble = !inDouble;
|
|
227
282
|
}
|
|
228
283
|
}
|
|
284
|
+
// Check if current character is a single quote [ds]
|
|
229
285
|
else if (line[j] === "'" && !inDouble) {
|
|
230
286
|
let escaped = false;
|
|
231
287
|
let k = j - 1;
|
|
@@ -239,10 +295,10 @@ function analyzeComments(lines, ext = '') {
|
|
|
239
295
|
}
|
|
240
296
|
}
|
|
241
297
|
}
|
|
242
|
-
// Increment character index [ds]
|
|
243
298
|
j++;
|
|
244
299
|
}
|
|
245
|
-
|
|
300
|
+
const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
|
|
301
|
+
if (resetsAtLineEnd) {
|
|
246
302
|
inSingle = false;
|
|
247
303
|
inDouble = false;
|
|
248
304
|
}
|
|
@@ -267,7 +323,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
267
323
|
return analysis;
|
|
268
324
|
}
|
|
269
325
|
|
|
270
|
-
/** Splices comments into
|
|
326
|
+
/** Splices comments into the given data [ds] */
|
|
271
327
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
272
328
|
const hasCRLF = data.includes('\r\n');
|
|
273
329
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -289,7 +345,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
289
345
|
let start = i;
|
|
290
346
|
let end = i;
|
|
291
347
|
while (end < originalLines.length && analysis[end].isInsideBlock) end++;
|
|
292
|
-
let blockStart = start - 1;
|
|
348
|
+
let blockStart = Math.max(0, start - 1);
|
|
293
349
|
let blockEnd = end - 1;
|
|
294
350
|
let hasDs = false;
|
|
295
351
|
for (let k = blockStart; k <= blockEnd; k++) {
|
|
@@ -316,6 +372,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
316
372
|
continue;
|
|
317
373
|
}
|
|
318
374
|
|
|
375
|
+
// Prune mode: remove all comments from the line [ds]
|
|
319
376
|
if (mode === 'prune') {
|
|
320
377
|
if (lineAnalysis.isPureComment) {
|
|
321
378
|
finalDeletions.add(lineNum);
|
|
@@ -390,6 +447,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
390
447
|
const indentation = indentMatch ? indentMatch[1] : '';
|
|
391
448
|
|
|
392
449
|
const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
|
|
450
|
+
// Add the comment to the list of comments [ds]
|
|
393
451
|
let trimmed = line.trimStart();
|
|
394
452
|
if (!trimmed) return '';
|
|
395
453
|
|
|
@@ -421,6 +479,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
421
479
|
const filteredText = filtered.map(line => line.text);
|
|
422
480
|
const filteredIndices = filtered.map(line => line.originalIndex);
|
|
423
481
|
|
|
482
|
+
// Check if the filtered text is equal to the original text [ds]
|
|
424
483
|
const textEqual = filteredText.every((text, idx) => {
|
|
425
484
|
const origIdx = filteredIndices[idx];
|
|
426
485
|
const originalLine = originalLines[origIdx];
|
|
@@ -459,7 +518,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
459
518
|
return annotated.map(line => line.text).join(lineEnding);
|
|
460
519
|
}
|
|
461
520
|
|
|
462
|
-
/** Runs the CLI interface
|
|
521
|
+
/** Runs the CLI interface [ds] */
|
|
463
522
|
async function runCLI() {
|
|
464
523
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
465
524
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -549,6 +608,8 @@ Options:
|
|
|
549
608
|
if (args.includes('--prune')) mode = 'prune';
|
|
550
609
|
const isDryRun = args.includes('--dry-run');
|
|
551
610
|
const isForce = args.includes('--force');
|
|
611
|
+
const hasOverwriteFlag = args.includes('--overwrite');
|
|
612
|
+
const hasKeepFlag = args.includes('--keep');
|
|
552
613
|
|
|
553
614
|
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
|
|
554
615
|
console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
|
|
@@ -579,24 +640,48 @@ Options:
|
|
|
579
640
|
let successCount = 0;
|
|
580
641
|
let failCount = 0;
|
|
581
642
|
|
|
643
|
+
const isOverwrite = (hasOverwriteFlag || config.autoPrune) && !hasKeepFlag;
|
|
644
|
+
|
|
645
|
+
let userIgnorePatterns = [];
|
|
646
|
+
try {
|
|
647
|
+
const ignorePath = path.join(process.cwd(), '.devsplainignore');
|
|
648
|
+
if (fs.existsSync(ignorePath)) {
|
|
649
|
+
const ignoreContent = fs.readFileSync(ignorePath, 'utf8');
|
|
650
|
+
userIgnorePatterns = ignoreContent.split(/\r?\n/)
|
|
651
|
+
.map(line => line.trim())
|
|
652
|
+
.filter(line => line && !line.startsWith('#'));
|
|
653
|
+
}
|
|
654
|
+
} catch(e) {}
|
|
655
|
+
|
|
656
|
+
const defaultIgnoredFolders = [
|
|
657
|
+
'node_modules', '.git', 'dist', 'build', 'out',
|
|
658
|
+
'.next', '.nuxt', '.svelte-kit',
|
|
659
|
+
'venv', 'env', '.venv',
|
|
660
|
+
'.vscode', '.idea', 'coverage',
|
|
661
|
+
'tests', '__tests__', 'fixtures'
|
|
662
|
+
];
|
|
663
|
+
const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
|
|
664
|
+
|
|
665
|
+
/** Checks if a given path is ignored [ds] */
|
|
666
|
+
function isPathIgnored(targetPath) {
|
|
667
|
+
const filename = path.basename(targetPath);
|
|
668
|
+
for (const pattern of allIgnored) {
|
|
669
|
+
const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
|
|
670
|
+
if (filename === cleanPattern) return true;
|
|
671
|
+
if (pattern.startsWith('*') && filename.endsWith(pattern.slice(1))) return true;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Processes a given path [ds] */
|
|
582
677
|
async function processPath(targetPath) {
|
|
583
|
-
// Process a directory or file path [ds]
|
|
584
678
|
const stats = fs.statSync(targetPath);
|
|
585
679
|
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
'node_modules', '.git', 'dist', 'build', 'out',
|
|
590
|
-
'.next', '.nuxt', '.svelte-kit',
|
|
591
|
-
'venv', 'env', '.venv',
|
|
592
|
-
'.vscode', '.idea', 'coverage',
|
|
593
|
-
'tests', '__tests__', 'fixtures'
|
|
594
|
-
];
|
|
595
|
-
|
|
596
|
-
if (ignoredFolders.includes(folderName)) {
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
680
|
+
if (isPathIgnored(targetPath)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
599
683
|
|
|
684
|
+
if (stats.isDirectory()) {
|
|
600
685
|
console.log(`\n Scanning directory: ${targetPath}`);
|
|
601
686
|
const items = fs.readdirSync(targetPath);
|
|
602
687
|
for (const item of items) {
|
|
@@ -628,7 +713,8 @@ Options:
|
|
|
628
713
|
let comments = [];
|
|
629
714
|
let commentedCode;
|
|
630
715
|
if (mode !== 'clean' && mode !== 'prune') {
|
|
631
|
-
const
|
|
716
|
+
const preProcessMode = isOverwrite ? 'prune' : 'clean';
|
|
717
|
+
const cleanData = spliceComments(data, [], preProcessMode, ext);
|
|
632
718
|
comments = await getComments(cleanData, filename, config, mode);
|
|
633
719
|
commentedCode = spliceComments(cleanData, comments, mode, ext);
|
|
634
720
|
} else {
|
|
@@ -677,7 +763,6 @@ Options:
|
|
|
677
763
|
rl.close();
|
|
678
764
|
}
|
|
679
765
|
|
|
680
|
-
// Run the CLI interface if this script is the main module [ds]
|
|
681
766
|
if (require.main === module) {
|
|
682
767
|
runCLI().catch(err => {
|
|
683
768
|
console.error(err);
|
package/bin/post-commit.js
CHANGED
|
@@ -3,60 +3,61 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spliceComments } = require('./cli');
|
|
5
5
|
|
|
6
|
-
/** Main execution block [ds] */
|
|
6
|
+
/** Main execution block for post-commit hook [ds] */
|
|
7
7
|
try {
|
|
8
|
-
//
|
|
8
|
+
// Check if SKIP_DEVSPLAIN environment variable is set [ds]
|
|
9
9
|
if (process.env.SKIP_DEVSPLAIN) {
|
|
10
10
|
console.log('[devsplain] SKIP_DEVSPLAIN is set. Bypassing AI generation.');
|
|
11
11
|
process.exit(0);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Determine if Git is in a rebasing, merging, or cherry-picking state [ds]
|
|
15
15
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
16
16
|
const isRebasing = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
17
17
|
const isMerging = fs.existsSync(path.join(gitDir, 'MERGE_HEAD'));
|
|
18
18
|
const isCherryPicking = fs.existsSync(path.join(gitDir, 'CHERRY_PICK_HEAD'));
|
|
19
19
|
|
|
20
|
+
// Exit if Git is in a rebasing, merging, or cherry-picking state to avoid history conflicts [ds]
|
|
20
21
|
if (isRebasing || isMerging || isCherryPicking) {
|
|
21
22
|
console.log('[devsplain] Skipping AI comment generation during git rebase/merge/cherry-pick to avoid history conflicts.');
|
|
22
23
|
process.exit(0);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
// Retrieve the last commit message [ds]
|
|
26
27
|
const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
|
|
27
28
|
if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
|
|
28
29
|
process.exit(0);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
// Get the last commit
|
|
32
|
+
// Get a list of changed files in the last commit [ds]
|
|
32
33
|
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
|
|
33
34
|
if (!changedFilesStr) {
|
|
34
35
|
process.exit(0);
|
|
35
36
|
}
|
|
36
37
|
const changedFiles = changedFilesStr.split(/\r?\n/);
|
|
37
38
|
|
|
38
|
-
/** List of valid file extensions for commenting [ds] */
|
|
39
|
+
/** List of valid file extensions for auto-commenting [ds] */
|
|
39
40
|
const validExtensions = [
|
|
40
41
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
41
42
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
42
43
|
'.swift', '.kt', '.dart', '.sh'
|
|
43
44
|
];
|
|
44
45
|
|
|
45
|
-
/** Filter files to
|
|
46
|
+
/** Filter function to determine which files to auto-comment [ds] */
|
|
46
47
|
const filesToComment = changedFiles.filter(file => {
|
|
47
48
|
const ext = path.extname(file).toLowerCase();
|
|
48
49
|
const isIgnored = file.includes('node_modules/') || file.includes('tests/') || file.includes('__tests__/') || file.includes('fixtures/');
|
|
49
50
|
return validExtensions.includes(ext) && fs.existsSync(file) && !isIgnored;
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
// Check if there are any files to comment [ds]
|
|
53
53
|
if (filesToComment.length === 0) {
|
|
54
54
|
process.exit(0);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Log the number of files found for auto-commenting [ds]
|
|
57
58
|
console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
|
|
58
59
|
|
|
59
|
-
// Parse command
|
|
60
|
+
// Parse command-line arguments for mode flags [ds]
|
|
60
61
|
const args = process.argv.slice(2);
|
|
61
62
|
let modeFlag = '';
|
|
62
63
|
if (args.includes('--light')) modeFlag = ' --light';
|
|
@@ -65,8 +66,9 @@ try {
|
|
|
65
66
|
let commentedAny = false;
|
|
66
67
|
const successfullyCommentedFiles = [];
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
+
/** Loop through each file to auto-comment [ds] */
|
|
69
70
|
for (const file of filesToComment) {
|
|
71
|
+
// Attempt to read file contents and previous version [ds]
|
|
70
72
|
try {
|
|
71
73
|
const ext = path.extname(file).toLowerCase();
|
|
72
74
|
const contentHead = fs.readFileSync(file, 'utf8');
|
|
@@ -79,11 +81,12 @@ try {
|
|
|
79
81
|
} catch (prevErr) {
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
//
|
|
84
|
+
// Check if file contents have changed (ignoring comments) [ds]
|
|
83
85
|
if (contentPrev) {
|
|
84
86
|
const cleanHead = spliceComments(contentHead, [], 'prune', ext);
|
|
85
87
|
const cleanPrev = spliceComments(contentPrev, [], 'prune', ext);
|
|
86
|
-
|
|
88
|
+
const isExplicitOverwrite = !!process.env.DS_OVER;
|
|
89
|
+
if (cleanHead === cleanPrev && !isExplicitOverwrite) {
|
|
87
90
|
console.log(`[devsplain] Skipping ${file}: commit contains only comment changes.`);
|
|
88
91
|
continue;
|
|
89
92
|
}
|
|
@@ -91,11 +94,16 @@ try {
|
|
|
91
94
|
} catch (cleanErr) {
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
//
|
|
97
|
+
// Log and attempt to auto-comment the current file [ds]
|
|
95
98
|
console.log(`[devsplain] Automatically commenting file: ${file}`);
|
|
96
99
|
try {
|
|
100
|
+
let extraFlags = '';
|
|
101
|
+
if (process.env.DS_OVER) extraFlags += ' --overwrite';
|
|
102
|
+
if (process.env.DS_KEEP) extraFlags += ' --keep';
|
|
103
|
+
|
|
104
|
+
// Construct the command to run the auto-commenting CLI [ds]
|
|
97
105
|
const cliPath = path.join(__dirname, 'cli.js');
|
|
98
|
-
execSync(`node "${cliPath}" "${file}" --force${modeFlag}`, { stdio: 'inherit' });
|
|
106
|
+
execSync(`node "${cliPath}" "${file}" --force${modeFlag}${extraFlags}`, { stdio: 'inherit' });
|
|
99
107
|
commentedAny = true;
|
|
100
108
|
successfullyCommentedFiles.push(file);
|
|
101
109
|
} catch (err) {
|
|
@@ -103,16 +111,14 @@ try {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
/**
|
|
114
|
+
/** If any files were successfully commented, stage and commit changes [ds] */
|
|
107
115
|
if (commentedAny) {
|
|
108
|
-
// Only stage the exact files that the AI touched to avoid accidentally committing unstaged work [ds]
|
|
109
116
|
for (const file of successfullyCommentedFiles) {
|
|
110
117
|
try {
|
|
111
118
|
execSync(`git add "${file}"`);
|
|
112
119
|
} catch (addErr) {}
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
// Check if there are actually staged changes now [ds]
|
|
116
122
|
const stagedChanges = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
117
123
|
if (stagedChanges.length > 0) {
|
|
118
124
|
console.log('[devsplain] Staging and committing auto-generated comments...');
|
|
@@ -120,7 +126,7 @@ try {
|
|
|
120
126
|
console.log('[devsplain] Comments committed successfully! Rollback via: git reset --hard HEAD~1');
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
|
-
|
|
129
|
+
// Catch and log any errors that occur during execution [ds]
|
|
124
130
|
} catch (e) {
|
|
125
131
|
console.warn(`[devsplain] Warning: post-commit hook script failed: ${e.message}`);
|
|
126
132
|
}
|
package/bin/setup-hook.js
CHANGED
|
@@ -3,36 +3,44 @@ 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();
|
|
10
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
10
11
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
11
12
|
if (!fs.existsSync(hooksDir)) {
|
|
12
13
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
let modeChoice = '1';
|
|
16
|
-
// Check if process is
|
|
17
|
+
// Check if process is run in a TTY environment [ds]
|
|
17
18
|
if (process.stdout.isTTY) {
|
|
18
19
|
const rl = readline.createInterface({
|
|
19
20
|
input: process.stdin,
|
|
20
21
|
output: process.stdout
|
|
21
22
|
});
|
|
22
|
-
// Create a readline interface for user input [ds]
|
|
23
23
|
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
24
24
|
|
|
25
|
-
//
|
|
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
|
|
|
35
|
-
// Determine
|
|
43
|
+
// Determine mode arguments based on user choice [ds]
|
|
36
44
|
let modeArgs = '';
|
|
37
45
|
if (modeChoice === '2') {
|
|
38
46
|
modeArgs = ' --light';
|
|
@@ -40,7 +48,7 @@ async function installHooks() {
|
|
|
40
48
|
modeArgs = ' --full';
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
// Define the
|
|
51
|
+
// Define the pre-commit hook script [ds]
|
|
44
52
|
const preCommitHookPath = path.join(hooksDir, 'pre-commit');
|
|
45
53
|
const preCommitContent = `#!/bin/sh
|
|
46
54
|
# devsplain native pre-commit hook
|
|
@@ -49,16 +57,16 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
|
|
|
49
57
|
npm test || exit 1
|
|
50
58
|
fi
|
|
51
59
|
`;
|
|
60
|
+
// Write the pre-commit hook to the Git hooks directory [ds]
|
|
52
61
|
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
53
|
-
// Attempt to set the execute permissions for the pre-commit hook file [ds]
|
|
54
62
|
try {
|
|
55
63
|
fs.chmodSync(preCommitHookPath, 0o755);
|
|
56
64
|
} catch (err) {}
|
|
57
65
|
|
|
58
|
-
// Define the
|
|
66
|
+
// Define the post-commit script path [ds]
|
|
59
67
|
const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
|
|
60
68
|
|
|
61
|
-
// Define the
|
|
69
|
+
// Define the post-commit hook script [ds]
|
|
62
70
|
const postCommitHookPath = path.join(hooksDir, 'post-commit');
|
|
63
71
|
const postCommitContent = `#!/bin/sh
|
|
64
72
|
# devsplain native post-commit hook
|
|
@@ -66,22 +74,46 @@ echo "Auto-generating comments for files in the last commit..."
|
|
|
66
74
|
node "${postCommitScript}"${modeArgs} || exit 1
|
|
67
75
|
`;
|
|
68
76
|
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
69
|
-
// Attempt to set the execute permissions for the post-commit hook file [ds]
|
|
70
77
|
try {
|
|
71
78
|
fs.chmodSync(postCommitHookPath, 0o755);
|
|
72
79
|
} catch (err) {}
|
|
73
80
|
|
|
74
|
-
//
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
81
|
+
// Inform the user about the successful installation of the post-commit hook [ds]
|
|
82
|
+
console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
|
|
83
|
+
|
|
84
|
+
// Check if a .devsplainignore file exists in the Git root directory [ds]
|
|
85
|
+
const ignorePath = path.join(gitRoot, '.devsplainignore');
|
|
86
|
+
if (!fs.existsSync(ignorePath)) {
|
|
87
|
+
const defaultIgnore = `node_modules/
|
|
88
|
+
.git/
|
|
89
|
+
dist/
|
|
90
|
+
build/
|
|
91
|
+
out/
|
|
92
|
+
.next/
|
|
93
|
+
.nuxt/
|
|
94
|
+
.svelte-kit/
|
|
95
|
+
venv/
|
|
96
|
+
env/
|
|
97
|
+
.venv/
|
|
98
|
+
.vscode/
|
|
99
|
+
.idea/
|
|
100
|
+
coverage/
|
|
101
|
+
tests/
|
|
102
|
+
__tests__/
|
|
103
|
+
fixtures/
|
|
104
|
+
`;
|
|
105
|
+
fs.writeFileSync(ignorePath, defaultIgnore);
|
|
106
|
+
console.log(`[devsplain] Created default .devsplainignore file at: ${ignorePath}`);
|
|
107
|
+
}
|
|
77
108
|
} catch (e) {
|
|
78
109
|
console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
|
|
110
|
+
console.warn(e.message);
|
|
79
111
|
}
|
|
80
112
|
}
|
|
81
113
|
|
|
82
|
-
// Check if
|
|
114
|
+
// Check if the script is run as the main module [ds]
|
|
83
115
|
if (require.main === module) {
|
|
84
116
|
installHooks();
|
|
85
117
|
}
|
|
86
|
-
// Export the installHooks function
|
|
118
|
+
// Export the installHooks function for external use [ds]
|
|
87
119
|
module.exports = installHooks;
|
package/lib/config.js
CHANGED
|
@@ -4,7 +4,36 @@ 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
|
+
function askSecret(query) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout
|
|
13
|
+
});
|
|
14
|
+
process.stdout.write(query);
|
|
15
|
+
process.stdin.setRawMode(true);
|
|
16
|
+
let key = '';
|
|
17
|
+
const onData = (char) => {
|
|
18
|
+
char = char.toString();
|
|
19
|
+
if (char === '\n' || char === '\r') {
|
|
20
|
+
process.stdin.setRawMode(false);
|
|
21
|
+
process.stdin.removeListener('data', onData);
|
|
22
|
+
process.stdout.write('\n');
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(key);
|
|
25
|
+
} else if (char === '\u0003') { // Ctrl+C
|
|
26
|
+
process.exit();
|
|
27
|
+
} else {
|
|
28
|
+
key += char;
|
|
29
|
+
process.stdout.write('*');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
process.stdin.on('data', onData);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Retrieves the configuration, either from environment variables or by prompting the user [ds] */
|
|
8
37
|
async function getConfig(forceWizard = false) {
|
|
9
38
|
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
10
39
|
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
@@ -18,24 +47,24 @@ async function getConfig(forceWizard = false) {
|
|
|
18
47
|
};
|
|
19
48
|
}
|
|
20
49
|
|
|
21
|
-
//
|
|
50
|
+
// If the configuration file does not exist or forceWizard is true, prompt the user to configure [ds]
|
|
22
51
|
if (!fs.existsSync(configPath) || forceWizard) {
|
|
23
|
-
|
|
52
|
+
let rl = readline.createInterface({
|
|
24
53
|
input: process.stdin,
|
|
25
54
|
output: process.stdout
|
|
26
55
|
});
|
|
27
|
-
|
|
56
|
+
let askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
28
57
|
|
|
29
58
|
let config = null;
|
|
30
59
|
let confirmed = false;
|
|
31
60
|
|
|
32
|
-
// Continuously prompt user
|
|
61
|
+
// Continuously prompt the user until the configuration is confirmed [ds]
|
|
33
62
|
while (!confirmed) {
|
|
34
63
|
let baseUrl = "";
|
|
35
64
|
let model = "";
|
|
36
65
|
let provider = "";
|
|
37
66
|
|
|
38
|
-
// Display available AI
|
|
67
|
+
// Display the list of available AI providers [ds]
|
|
39
68
|
console.log("\nWhich AI Provider Do You want to use?");
|
|
40
69
|
console.log("1. Groq (Free, Fast, Llama-3)");
|
|
41
70
|
console.log("2. Gemini (Free Tier)");
|
|
@@ -43,10 +72,8 @@ async function getConfig(forceWizard = false) {
|
|
|
43
72
|
console.log("4. Custom (Ollama, local, etc)");
|
|
44
73
|
console.log("5. Claude (Anthropic)");
|
|
45
74
|
|
|
46
|
-
// Get user's selected AI provider option [ds]
|
|
47
75
|
const choice = await askQuestion("Select (1-5): ");
|
|
48
76
|
|
|
49
|
-
// Handle selected AI provider option [ds]
|
|
50
77
|
if (choice === '1') {
|
|
51
78
|
provider = 'groq';
|
|
52
79
|
baseUrl = 'https://api.groq.com/openai';
|
|
@@ -67,8 +94,16 @@ async function getConfig(forceWizard = false) {
|
|
|
67
94
|
model = customModel.trim() || 'gpt-4o';
|
|
68
95
|
} else if (choice === '4') {
|
|
69
96
|
provider = 'custom';
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
72
107
|
} else if (choice === '5') {
|
|
73
108
|
provider = 'claude';
|
|
74
109
|
baseUrl = 'https://api.anthropic.com';
|
|
@@ -80,38 +115,82 @@ async function getConfig(forceWizard = false) {
|
|
|
80
115
|
continue;
|
|
81
116
|
}
|
|
82
117
|
|
|
83
|
-
//
|
|
84
|
-
|
|
118
|
+
// Prompt the user for an API key [ds]
|
|
119
|
+
let apiKey = '';
|
|
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}'.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
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
|
+
}
|
|
85
158
|
|
|
86
|
-
// Display
|
|
159
|
+
// Display a summary of the configuration [ds]
|
|
87
160
|
console.log("\n--- Configuration Summary ---");
|
|
88
|
-
console.log(`Provider:
|
|
89
|
-
console.log(`Model:
|
|
90
|
-
console.log(`Base URL:
|
|
91
|
-
console.log(`API Key:
|
|
161
|
+
console.log(`Provider: ${provider}`);
|
|
162
|
+
console.log(`Model: ${model}`);
|
|
163
|
+
console.log(`Base URL: ${baseUrl || 'N/A'}`);
|
|
164
|
+
console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
|
|
165
|
+
console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
|
|
92
166
|
console.log("-----------------------------\n");
|
|
93
167
|
|
|
94
|
-
// Confirm configuration with user [ds]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
// Confirm the configuration 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'.");
|
|
106
185
|
}
|
|
107
186
|
}
|
|
108
187
|
|
|
109
188
|
rl.close();
|
|
110
189
|
|
|
111
|
-
// Write configuration to file [ds]
|
|
190
|
+
// Write the configuration to the config file [ds]
|
|
112
191
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
113
192
|
try {
|
|
114
|
-
// Set
|
|
193
|
+
// Set the permissions of the config file to prevent other users from reading it [ds]
|
|
115
194
|
if (process.platform !== 'win32') {
|
|
116
195
|
fs.chmodSync(configPath, 0o600);
|
|
117
196
|
}
|
|
@@ -120,11 +199,9 @@ async function getConfig(forceWizard = false) {
|
|
|
120
199
|
|
|
121
200
|
return config;
|
|
122
201
|
} else {
|
|
123
|
-
// Read existing configuration from file [ds]
|
|
124
202
|
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
125
203
|
return JSON.parse(rawData);
|
|
126
204
|
}
|
|
127
205
|
}
|
|
128
206
|
|
|
129
|
-
|
|
130
|
-
module.exports = { getConfig };
|
|
207
|
+
module.exports = { getConfig };
|
package/package.json
CHANGED