devsplain 1.2.0 → 1.5.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/LICENSE +21 -0
- package/README.md +115 -41
- package/bin/cli.js +591 -195
- package/bin/post-commit.js +96 -0
- package/bin/setup-hook.js +79 -0
- package/lib/config.js +124 -80
- package/lib/llm.js +221 -139
- package/package.json +43 -43
package/bin/cli.js
CHANGED
|
@@ -1,196 +1,592 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
|
|
2
|
+
const { getComments } = require('../lib/llm.js');
|
|
3
|
+
const { getConfig } = require('../lib/config.js');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
let rl;
|
|
10
|
+
let askQuestion;
|
|
11
|
+
|
|
12
|
+
/** Checks if the current Git repository has uncommitted changes */
|
|
13
|
+
function isGitDirty() {
|
|
14
|
+
try {
|
|
15
|
+
const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
16
|
+
if (gitDir === 'true') {
|
|
17
|
+
const status = execSync('git status --porcelain', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
18
|
+
return status.length > 0;
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Determines if a specific line index is inside a string or multiline literal */
|
|
26
|
+
function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
27
|
+
const isPython = ext.toLowerCase() === '.py';
|
|
28
|
+
let inBacktick = false;
|
|
29
|
+
let inTripleDouble = false;
|
|
30
|
+
let inTripleSingle = false;
|
|
31
|
+
let inSingle = false;
|
|
32
|
+
let inDouble = false;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < targetLineIndex; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
let j = 0;
|
|
37
|
+
while (j < line.length) {
|
|
38
|
+
if (isPython) {
|
|
39
|
+
if (!inTripleSingle) {
|
|
40
|
+
if (line.slice(j, j + 3) === '"""') {
|
|
41
|
+
inTripleDouble = !inTripleDouble;
|
|
42
|
+
j += 3;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!inTripleDouble) {
|
|
47
|
+
if (line.slice(j, j + 3) === "'''") {
|
|
48
|
+
inTripleSingle = !inTripleSingle;
|
|
49
|
+
j += 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (!inSingle && !inDouble && line[j] === '`') {
|
|
55
|
+
let escaped = false;
|
|
56
|
+
let k = j - 1;
|
|
57
|
+
while (k >= 0 && line[k] === '\\') {
|
|
58
|
+
escaped = !escaped;
|
|
59
|
+
k--;
|
|
60
|
+
}
|
|
61
|
+
if (!escaped) {
|
|
62
|
+
inBacktick = !inBacktick;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!inBacktick) {
|
|
66
|
+
if (line[j] === '"' && !inSingle) {
|
|
67
|
+
let escaped = false;
|
|
68
|
+
let k = j - 1;
|
|
69
|
+
while (k >= 0 && line[k] === '\\') {
|
|
70
|
+
escaped = !escaped;
|
|
71
|
+
k--;
|
|
72
|
+
}
|
|
73
|
+
if (!escaped) {
|
|
74
|
+
inDouble = !inDouble;
|
|
75
|
+
}
|
|
76
|
+
} else if (line[j] === "'" && !inDouble) {
|
|
77
|
+
let escaped = false;
|
|
78
|
+
let k = j - 1;
|
|
79
|
+
while (k >= 0 && line[k] === '\\') {
|
|
80
|
+
escaped = !escaped;
|
|
81
|
+
k--;
|
|
82
|
+
}
|
|
83
|
+
if (!escaped) {
|
|
84
|
+
inSingle = !inSingle;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
j++;
|
|
90
|
+
}
|
|
91
|
+
if (!isPython) {
|
|
92
|
+
inSingle = false;
|
|
93
|
+
inDouble = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Analyzes code to identify pure comment lines and block boundaries */
|
|
100
|
+
function analyzeComments(lines, ext = '') {
|
|
101
|
+
const isPython = ext.toLowerCase() === '.py';
|
|
102
|
+
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
103
|
+
const analysis = [];
|
|
104
|
+
let inBacktick = false;
|
|
105
|
+
let inTripleDouble = false;
|
|
106
|
+
let inTripleSingle = false;
|
|
107
|
+
let inSingle = false;
|
|
108
|
+
let inDouble = false;
|
|
109
|
+
let inBlockJS = false;
|
|
110
|
+
let inBlockHTML = false;
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i];
|
|
113
|
+
let commentStartIndex = -1;
|
|
114
|
+
let isInsideBlockStart = inBlockJS || inBlockHTML;
|
|
115
|
+
let j = 0;
|
|
116
|
+
while (j < line.length) {
|
|
117
|
+
if (inBlockJS) {
|
|
118
|
+
if (line.slice(j, j + 2) === '*/') {
|
|
119
|
+
inBlockJS = false;
|
|
120
|
+
j += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
j++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (inBlockHTML) {
|
|
127
|
+
if (line.slice(j, j + 3) === '-->') {
|
|
128
|
+
inBlockHTML = false;
|
|
129
|
+
j += 3;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
j++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!inSingle && !inDouble && !inBacktick && !inTripleSingle && !inTripleDouble) {
|
|
136
|
+
if (isPython) {
|
|
137
|
+
if (line[j] === '#') {
|
|
138
|
+
commentStartIndex = j;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
} else if (isHTML) {
|
|
142
|
+
if (line.slice(j, j + 4) === '<!--') {
|
|
143
|
+
commentStartIndex = j;
|
|
144
|
+
inBlockHTML = true;
|
|
145
|
+
j += 4;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (line.slice(j, j + 2) === '//') {
|
|
149
|
+
commentStartIndex = j;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
if (line.slice(j, j + 2) === '//') {
|
|
154
|
+
commentStartIndex = j;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (line.slice(j, j + 2) === '/*') {
|
|
158
|
+
commentStartIndex = j;
|
|
159
|
+
inBlockJS = true;
|
|
160
|
+
j += 2;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (line[j] === '#') {
|
|
164
|
+
commentStartIndex = j;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (isPython) {
|
|
170
|
+
if (!inTripleSingle) {
|
|
171
|
+
if (line.slice(j, j + 3) === '"""') {
|
|
172
|
+
inTripleDouble = !inTripleDouble;
|
|
173
|
+
j += 3;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!inTripleDouble) {
|
|
178
|
+
if (line.slice(j, j + 3) === "'''") {
|
|
179
|
+
inTripleSingle = !inTripleSingle;
|
|
180
|
+
j += 3;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (!inSingle && !inDouble) {
|
|
186
|
+
if (line[j] === '`') {
|
|
187
|
+
let escaped = false;
|
|
188
|
+
let k = j - 1;
|
|
189
|
+
while (k >= 0 && line[k] === '\\') {
|
|
190
|
+
escaped = !escaped;
|
|
191
|
+
k--;
|
|
192
|
+
}
|
|
193
|
+
if (!escaped) {
|
|
194
|
+
inBacktick = !inBacktick;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!inBacktick) {
|
|
199
|
+
if (line[j] === '"' && !inSingle) {
|
|
200
|
+
let escaped = false;
|
|
201
|
+
let k = j - 1;
|
|
202
|
+
while (k >= 0 && line[k] === '\\') {
|
|
203
|
+
escaped = !escaped;
|
|
204
|
+
k--;
|
|
205
|
+
}
|
|
206
|
+
if (!escaped) {
|
|
207
|
+
inDouble = !inDouble;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (line[j] === "'" && !inDouble) {
|
|
211
|
+
let escaped = false;
|
|
212
|
+
let k = j - 1;
|
|
213
|
+
while (k >= 0 && line[k] === '\\') {
|
|
214
|
+
escaped = !escaped;
|
|
215
|
+
k--;
|
|
216
|
+
}
|
|
217
|
+
if (!escaped) {
|
|
218
|
+
inSingle = !inSingle;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
j++;
|
|
224
|
+
}
|
|
225
|
+
if (!isPython) {
|
|
226
|
+
inSingle = false;
|
|
227
|
+
inDouble = false;
|
|
228
|
+
}
|
|
229
|
+
const isEntirelyInsideBlock = isInsideBlockStart && (inBlockJS || inBlockHTML || (commentStartIndex === -1));
|
|
230
|
+
let isPureComment = false;
|
|
231
|
+
if (isEntirelyInsideBlock) {
|
|
232
|
+
isPureComment = true;
|
|
233
|
+
} else if (commentStartIndex !== -1) {
|
|
234
|
+
const beforeComment = line.slice(0, commentStartIndex).trim();
|
|
235
|
+
if (beforeComment === '') {
|
|
236
|
+
isPureComment = true;
|
|
237
|
+
}
|
|
238
|
+
} else if (line.trim() === '') {
|
|
239
|
+
isPureComment = true;
|
|
240
|
+
}
|
|
241
|
+
analysis.push({
|
|
242
|
+
isPureComment,
|
|
243
|
+
commentStartIndex,
|
|
244
|
+
isInsideBlock: isEntirelyInsideBlock || isInsideBlockStart
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return analysis;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Splicers or removes comments from source data based on the requested mode */
|
|
251
|
+
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
252
|
+
const hasCRLF = data.includes('\r\n');
|
|
253
|
+
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
254
|
+
const originalLines = data.split(/\r?\n/);
|
|
255
|
+
const sortedComments = [...comments].sort((a, b) => b.line - a.line);
|
|
256
|
+
const validComments = sortedComments.filter(c => c.line >= 1 && c.line <= originalLines.length + 1);
|
|
257
|
+
|
|
258
|
+
const annotated = originalLines.map((text, index) => ({ text, originalIndex: index }));
|
|
259
|
+
let analysis = null;
|
|
260
|
+
|
|
261
|
+
if (mode === 'clean') {
|
|
262
|
+
analysis = analyzeComments(originalLines, ext);
|
|
263
|
+
const finalDeletions = new Set();
|
|
264
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
265
|
+
const lineNum = i + 1;
|
|
266
|
+
if (analysis[i].isPureComment) {
|
|
267
|
+
finalDeletions.add(lineNum);
|
|
268
|
+
} else if (analysis[i].commentStartIndex !== -1) {
|
|
269
|
+
annotated[i].text = originalLines[i].slice(0, analysis[i].commentStartIndex).trimEnd();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const c of validComments) {
|
|
274
|
+
const lineIdx = c.line - 1;
|
|
275
|
+
if (lineIdx >= 0 && lineIdx < originalLines.length) {
|
|
276
|
+
finalDeletions.add(c.line);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const linesToDelete = Array.from(finalDeletions).sort((a, b) => b - a);
|
|
281
|
+
|
|
282
|
+
for (const lineNum of linesToDelete) {
|
|
283
|
+
const targetLine = originalLines[lineNum - 1];
|
|
284
|
+
if (!targetLine) continue;
|
|
285
|
+
const trimmedLine = targetLine.trim();
|
|
286
|
+
|
|
287
|
+
const lineAnalysis = analysis[lineNum - 1];
|
|
288
|
+
const isCommentLine =
|
|
289
|
+
lineAnalysis.isInsideBlock ||
|
|
290
|
+
lineAnalysis.isPureComment ||
|
|
291
|
+
trimmedLine.startsWith('//') ||
|
|
292
|
+
trimmedLine.startsWith('/*') ||
|
|
293
|
+
trimmedLine.startsWith('*') ||
|
|
294
|
+
trimmedLine.startsWith('#') ||
|
|
295
|
+
trimmedLine.startsWith('<!--') ||
|
|
296
|
+
trimmedLine.startsWith('-->') ||
|
|
297
|
+
trimmedLine.startsWith('--') ||
|
|
298
|
+
trimmedLine.endsWith('*/') ||
|
|
299
|
+
trimmedLine === '';
|
|
300
|
+
|
|
301
|
+
if (!isCommentLine) {
|
|
302
|
+
console.warn(`[devsplain] Safety Block: Refused to delete non-comment line ${lineNum}: "${trimmedLine}"`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
annotated.splice(lineNum - 1, 1);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
for (const c of validComments) {
|
|
310
|
+
if (isLineInsideString(originalLines, c.line - 1, ext)) {
|
|
311
|
+
console.warn(`[devsplain] Skipping comment insertion at line ${c.line} to avoid string literal corruption.`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const targetLine = originalLines[c.line - 1] || '';
|
|
316
|
+
const indentMatch = targetLine.match(/^([ \t]*)/);
|
|
317
|
+
const indentation = indentMatch ? indentMatch[1] : '';
|
|
318
|
+
|
|
319
|
+
const commentLines = c.comment.split(/\r?\n/).map(line => {
|
|
320
|
+
const trimmed = line.trimStart();
|
|
321
|
+
if (!trimmed) return '';
|
|
322
|
+
if (trimmed.startsWith('*') && !trimmed.startsWith('*/') && !trimmed.startsWith('/*')) {
|
|
323
|
+
return indentation + ' ' + trimmed;
|
|
324
|
+
}
|
|
325
|
+
return indentation + trimmed;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const commentObjects = commentLines.map(line => ({ text: line, originalIndex: -1 }));
|
|
329
|
+
annotated.splice(c.line - 1, 0, ...commentObjects);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const filtered = annotated.filter(line => line.originalIndex !== -1);
|
|
334
|
+
const filteredText = filtered.map(line => line.text);
|
|
335
|
+
const filteredIndices = filtered.map(line => line.originalIndex);
|
|
336
|
+
|
|
337
|
+
const textEqual = filteredText.every((text, idx) => {
|
|
338
|
+
const origIdx = filteredIndices[idx];
|
|
339
|
+
const originalLine = originalLines[origIdx];
|
|
340
|
+
if (text === originalLine) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
if (mode === 'clean' && analysis) {
|
|
344
|
+
const lineAnalysis = analysis[origIdx];
|
|
345
|
+
if (lineAnalysis && lineAnalysis.commentStartIndex !== -1 && !lineAnalysis.isPureComment) {
|
|
346
|
+
const expectedStripped = originalLine.slice(0, lineAnalysis.commentStartIndex).trimEnd();
|
|
347
|
+
if (text === expectedStripped) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
let indicesSequential = true;
|
|
356
|
+
for (let i = 1; i < filteredIndices.length; i++) {
|
|
357
|
+
if (filteredIndices[i] <= filteredIndices[i - 1]) {
|
|
358
|
+
indicesSequential = false;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!textEqual || !indicesSequential) {
|
|
364
|
+
console.error("\nSafety Assertion Failed: Spliced code does not match original code minus comments!");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return annotated.map(line => line.text).join(lineEnding);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Main CLI entry point handler */
|
|
372
|
+
async function runCLI() {
|
|
373
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
374
|
+
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
375
|
+
|
|
376
|
+
const args = process.argv.slice(2);
|
|
377
|
+
|
|
378
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
379
|
+
console.log(`
|
|
380
|
+
devsplain - Universal Polyglot AI Code Commenter
|
|
381
|
+
|
|
382
|
+
Usage:
|
|
383
|
+
devsplain <file-or-directory> [options]
|
|
384
|
+
|
|
385
|
+
Options:
|
|
386
|
+
--light Add ONLY JSDoc/block comments above functions (minimalist)
|
|
387
|
+
--full Add detailed JSDoc/block comments and inline comments
|
|
388
|
+
--dry-run Preview comments without writing to file
|
|
389
|
+
--force Bypass the dirty Git tree safety check
|
|
390
|
+
--provider <name> Override AI provider (gemini, groq, openai, custom)
|
|
391
|
+
--model <name> Override AI model name
|
|
392
|
+
--api-key <key> Override API key for the provider
|
|
393
|
+
--base-url <url> Override base URL for custom APIs
|
|
394
|
+
--config Force run the configuration setup wizard
|
|
395
|
+
--setup-hook Install Git pre-commit and post-commit hooks in repository
|
|
396
|
+
--help, -h Show this help message
|
|
397
|
+
--version, -v Show version information
|
|
398
|
+
`);
|
|
399
|
+
rl.close();
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
404
|
+
const pkg = require('../package.json');
|
|
405
|
+
console.log(`devsplain v${pkg.version}`);
|
|
406
|
+
rl.close();
|
|
407
|
+
process.exit(0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (args.includes('--config')) {
|
|
411
|
+
rl.close();
|
|
412
|
+
await getConfig(true);
|
|
413
|
+
console.log("Success: Configuration updated successfully!");
|
|
414
|
+
process.exit(0);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (args.includes('--setup-hook')) {
|
|
418
|
+
rl.close();
|
|
419
|
+
require('./setup-hook.js');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const getArgValue = (flag) => {
|
|
424
|
+
const index = args.indexOf(flag);
|
|
425
|
+
if (index !== -1 && index + 1 < args.length) {
|
|
426
|
+
return args[index + 1];
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
let filepath = '.';
|
|
432
|
+
const flagKeys = ['--provider', '--model', '--api-key', '--base-url'];
|
|
433
|
+
for (let i = 0; i < args.length; i++) {
|
|
434
|
+
const arg = args[i];
|
|
435
|
+
if (arg.startsWith('--')) {
|
|
436
|
+
if (flagKeys.includes(arg)) {
|
|
437
|
+
i++;
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
filepath = arg;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!fs.existsSync(filepath)) {
|
|
446
|
+
console.log(`Error: The path '${filepath}' does not exist.`);
|
|
447
|
+
rl.close();
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let mode = 'default';
|
|
452
|
+
if (args.includes('--light')) mode = 'light';
|
|
453
|
+
if (args.includes('--full')) mode = 'full';
|
|
454
|
+
if (args.includes('--clean')) mode = 'clean';
|
|
455
|
+
const isDryRun = args.includes('--dry-run');
|
|
456
|
+
const isForce = args.includes('--force');
|
|
457
|
+
|
|
458
|
+
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
|
|
459
|
+
console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
|
|
460
|
+
rl.close();
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const config = await getConfig();
|
|
465
|
+
|
|
466
|
+
const cliProvider = getArgValue('--provider');
|
|
467
|
+
const cliModel = getArgValue('--model');
|
|
468
|
+
const cliApiKey = getArgValue('--api-key');
|
|
469
|
+
const cliBaseUrl = getArgValue('--base-url');
|
|
470
|
+
|
|
471
|
+
if (cliProvider) {
|
|
472
|
+
config.provider = cliProvider;
|
|
473
|
+
if (!cliModel) {
|
|
474
|
+
config.model = cliProvider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile';
|
|
475
|
+
}
|
|
476
|
+
if (!cliBaseUrl) {
|
|
477
|
+
config.baseUrl = cliProvider === 'gemini' ? null : (cliProvider === 'groq' ? 'https://api.groq.com/openai' : (cliProvider === 'openai' ? 'https://api.openai.com' : ''));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (cliModel) config.model = cliModel;
|
|
481
|
+
if (cliApiKey) config.apiKey = cliApiKey;
|
|
482
|
+
if (cliBaseUrl) config.baseUrl = cliBaseUrl;
|
|
483
|
+
|
|
484
|
+
let successCount = 0;
|
|
485
|
+
let failCount = 0;
|
|
486
|
+
|
|
487
|
+
/** Recursively traverses directories to process files */
|
|
488
|
+
async function processPath(targetPath) {
|
|
489
|
+
const stats = fs.statSync(targetPath);
|
|
490
|
+
|
|
491
|
+
if (stats.isDirectory()) {
|
|
492
|
+
const folderName = path.basename(targetPath);
|
|
493
|
+
const ignoredFolders = [
|
|
494
|
+
'node_modules', '.git', 'dist', 'build', 'out',
|
|
495
|
+
'.next', '.nuxt', '.svelte-kit',
|
|
496
|
+
'venv', 'env', '.venv',
|
|
497
|
+
'.vscode', '.idea', 'coverage'
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
if (ignoredFolders.includes(folderName)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(`\n Scanning directory: ${targetPath}`);
|
|
505
|
+
const items = fs.readdirSync(targetPath);
|
|
506
|
+
for (const item of items) {
|
|
507
|
+
const fullPath = path.join(targetPath, item);
|
|
508
|
+
await processPath(fullPath);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (stats.isFile()) {
|
|
512
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
513
|
+
const validExtensions = [
|
|
514
|
+
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
515
|
+
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
516
|
+
'.swift', '.kt', '.dart', '.sh'
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
if (!validExtensions.includes(ext)) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const filename = path.basename(targetPath);
|
|
524
|
+
const data = fs.readFileSync(targetPath, 'utf-8');
|
|
525
|
+
if (data.trim() === '') {
|
|
526
|
+
console.log(` Skipping ${filename} (Empty File)`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log(` Analyzing ${filename} in ${mode} mode...`);
|
|
531
|
+
try {
|
|
532
|
+
let comments = [];
|
|
533
|
+
let commentedCode;
|
|
534
|
+
if (mode !== 'clean') {
|
|
535
|
+
const cleanData = spliceComments(data, [], 'clean', ext);
|
|
536
|
+
comments = await getComments(cleanData, filename, config, mode);
|
|
537
|
+
commentedCode = spliceComments(cleanData, comments, mode, ext);
|
|
538
|
+
} else {
|
|
539
|
+
commentedCode = spliceComments(data, [], 'clean', ext);
|
|
540
|
+
}
|
|
541
|
+
if (isDryRun) {
|
|
542
|
+
console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
|
|
543
|
+
console.log(commentedCode);
|
|
544
|
+
console.log(`---------------------------------------\n`);
|
|
545
|
+
const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
|
|
546
|
+
if (answer.toLowerCase() === 'write') {
|
|
547
|
+
const tempPath = targetPath + '.tmp';
|
|
548
|
+
fs.writeFileSync(tempPath, commentedCode, 'utf8');
|
|
549
|
+
fs.renameSync(tempPath, targetPath);
|
|
550
|
+
console.log(` Successfully saved ${targetPath}`);
|
|
551
|
+
} else {
|
|
552
|
+
console.log(` Skipped ${targetPath}`);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
const tempPath = targetPath + '.tmp';
|
|
556
|
+
fs.writeFileSync(tempPath, commentedCode, 'utf8');
|
|
557
|
+
fs.renameSync(tempPath, targetPath);
|
|
558
|
+
console.log(` Successfully commented ${targetPath}`);
|
|
559
|
+
}
|
|
560
|
+
successCount++;
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.error(` Error processing ${filename}: ${err.message}`);
|
|
563
|
+
failCount++;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
await processPath(filepath);
|
|
569
|
+
|
|
570
|
+
if (failCount > 0 && successCount === 0) {
|
|
571
|
+
console.error("\nFailed: No files were successfully commented.");
|
|
572
|
+
rl.close();
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (successCount > 0 && failCount > 0) {
|
|
577
|
+
console.log(`\n All done! (Successfully commented: ${successCount}, Failed: ${failCount})`);
|
|
578
|
+
} else {
|
|
579
|
+
console.log("\n All done!");
|
|
580
|
+
}
|
|
581
|
+
rl.close();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// If running as a standalone script, start the CLI; otherwise export helpers
|
|
585
|
+
if (require.main === module) {
|
|
586
|
+
runCLI().catch(err => {
|
|
587
|
+
console.error(err);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
});
|
|
590
|
+
} else {
|
|
591
|
+
module.exports = { spliceComments, isLineInsideString };
|
|
196
592
|
}
|