devsplain 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- ![devsplain demo](sample.gif)
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 never rewrites executable code.
7
- If the original source cannot be reproduced exactly after comment insertion, the operation aborts.
5
+ ![devsplain demo](sample.gif)
8
6
 
9
- Unlike interactive AI editors, `devsplain` is a single-shot CLI designed for batch documentation passes, CI pipelines, and git hook automation—no agent overhead, no per-file confirmation loops.
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**: Natively parses JavaScript, JSX, TypeScript, TSX, HTML, CSS, SCSS, Vue, Svelte, Python, Java, C, C++, C#, Go, Ruby, PHP, Rust, Swift, Kotlin, Dart, and Shell scripts.
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 mathematically differentiate between your human-written manual comments and the AI-generated comments.
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 verification would require language-specific parser dependencies for every supported language.
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
- `devsplain` instead uses deterministic source-preservation verification:
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 you have configured `autoPrune` (aggressive overwrite) in your `~/.devsplainrc` but want to change the behavior for a specific commit:
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 working tree is dirty [ds] */
13
+ /** Checks if the Git repository is dirty [ds] */
14
14
  function isGitDirty() {
15
15
  try {
16
16
  const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
@@ -23,47 +23,109 @@ function isGitDirty() {
23
23
  return false;
24
24
  }
25
25
 
26
- /** Checks if a line is inside a string literal [ds] */
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
- } else {
55
- if (!inSingle && !inDouble && line[j] === '`') {
56
- let escaped = false;
57
- let k = j - 1;
58
- while (k >= 0 && line[k] === '\\') {
59
- escaped = !escaped;
60
- k--;
61
- }
62
- if (!escaped) {
63
- inBacktick = !inBacktick;
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
- if (!isPython) {
154
+ const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
155
+ if (resetsAtLineEnd) {
93
156
  inSingle = false;
94
157
  inDouble = false;
95
158
  }
@@ -97,7 +160,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
97
160
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble;
98
161
  }
99
162
 
100
- /** Analyzes comments in a list of lines [ds] */
163
+ /** Analyzes comments in a given list of lines [ds] */
101
164
  function analyzeComments(lines, ext = '') {
102
165
  const isPython = ext.toLowerCase() === '.py';
103
166
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -139,16 +202,6 @@ function analyzeComments(lines, ext = '') {
139
202
  commentStartIndex = j;
140
203
  break;
141
204
  }
142
- if (line.slice(j, j + 2) === '/*') {
143
- commentStartIndex = j;
144
- inBlockJS = true;
145
- j += 2;
146
- continue;
147
- }
148
- if (line.slice(j, j + 2) === '//') {
149
- commentStartIndex = j;
150
- break;
151
- }
152
205
  } else if (isHTML) {
153
206
  if (line.slice(j, j + 4) === '<!--') {
154
207
  commentStartIndex = j;
@@ -177,30 +230,33 @@ function analyzeComments(lines, ext = '') {
177
230
  j += 2;
178
231
  continue;
179
232
  }
180
- if (line[j] === '#') {
233
+ const isShellOrRuby = ['.sh', '.rb', '.php'].includes(ext.toLowerCase());
234
+ if (isShellOrRuby && line[j] === '#') {
181
235
  commentStartIndex = j;
182
236
  break;
183
237
  }
184
238
  }
185
239
  }
186
240
  if (isPython) {
187
- if (!inTripleSingle) {
241
+ if (!inTripleSingle && !inSingle && !inDouble) {
188
242
  if (line.slice(j, j + 3) === '"""') {
189
243
  inTripleDouble = !inTripleDouble;
190
244
  j += 3;
191
245
  continue;
192
246
  }
193
247
  }
194
- if (!inTripleDouble) {
248
+ if (!inTripleDouble && !inSingle && !inDouble) {
195
249
  if (line.slice(j, j + 3) === "'''") {
196
250
  inTripleSingle = !inTripleSingle;
197
251
  j += 3;
198
252
  continue;
199
253
  }
200
254
  }
201
- } else {
202
- if (!inSingle && !inDouble) {
203
- if (line[j] === '`') {
255
+ }
256
+ if (!inTripleSingle && !inTripleDouble) {
257
+ if (!isPython) {
258
+ if (!inSingle && !inDouble) {
259
+ if (line[j] === '`') {
204
260
  let escaped = false;
205
261
  let k = j - 1;
206
262
  while (k >= 0 && line[k] === '\\') {
@@ -212,6 +268,7 @@ function analyzeComments(lines, ext = '') {
212
268
  }
213
269
  }
214
270
  }
271
+ }
215
272
  if (!inBacktick) {
216
273
  if (line[j] === '"' && !inSingle) {
217
274
  let escaped = false;
@@ -224,6 +281,7 @@ function analyzeComments(lines, ext = '') {
224
281
  inDouble = !inDouble;
225
282
  }
226
283
  }
284
+ // Check if current character is a single quote [ds]
227
285
  else if (line[j] === "'" && !inDouble) {
228
286
  let escaped = false;
229
287
  let k = j - 1;
@@ -239,7 +297,8 @@ function analyzeComments(lines, ext = '') {
239
297
  }
240
298
  j++;
241
299
  }
242
- if (!isPython) {
300
+ const resetsAtLineEnd = ['.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.cs', '.go', '.swift', '.kt', '.dart'].includes(ext.toLowerCase());
301
+ if (resetsAtLineEnd) {
243
302
  inSingle = false;
244
303
  inDouble = false;
245
304
  }
@@ -264,7 +323,7 @@ function analyzeComments(lines, ext = '') {
264
323
  return analysis;
265
324
  }
266
325
 
267
- /** Splices comments into a list of lines [ds] */
326
+ /** Splices comments into the given data [ds] */
268
327
  function spliceComments(data, comments, mode = 'default', ext = '') {
269
328
  const hasCRLF = data.includes('\r\n');
270
329
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -286,7 +345,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
286
345
  let start = i;
287
346
  let end = i;
288
347
  while (end < originalLines.length && analysis[end].isInsideBlock) end++;
289
- let blockStart = start - 1;
348
+ let blockStart = Math.max(0, start - 1);
290
349
  let blockEnd = end - 1;
291
350
  let hasDs = false;
292
351
  for (let k = blockStart; k <= blockEnd; k++) {
@@ -313,6 +372,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
313
372
  continue;
314
373
  }
315
374
 
375
+ // Prune mode: remove all comments from the line [ds]
316
376
  if (mode === 'prune') {
317
377
  if (lineAnalysis.isPureComment) {
318
378
  finalDeletions.add(lineNum);
@@ -387,6 +447,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
387
447
  const indentation = indentMatch ? indentMatch[1] : '';
388
448
 
389
449
  const commentLines = c.comment.split(/\r?\n/).map((line, idx) => {
450
+ // Add the comment to the list of comments [ds]
390
451
  let trimmed = line.trimStart();
391
452
  if (!trimmed) return '';
392
453
 
@@ -418,6 +479,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
418
479
  const filteredText = filtered.map(line => line.text);
419
480
  const filteredIndices = filtered.map(line => line.originalIndex);
420
481
 
482
+ // Check if the filtered text is equal to the original text [ds]
421
483
  const textEqual = filteredText.every((text, idx) => {
422
484
  const origIdx = filteredIndices[idx];
423
485
  const originalLine = originalLines[origIdx];
@@ -456,7 +518,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
456
518
  return annotated.map(line => line.text).join(lineEnding);
457
519
  }
458
520
 
459
- /** Runs the CLI application [ds] */
521
+ /** Runs the CLI interface [ds] */
460
522
  async function runCLI() {
461
523
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
462
524
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -600,8 +662,8 @@ Options:
600
662
  ];
601
663
  const allIgnored = [...defaultIgnoredFolders, ...userIgnorePatterns];
602
664
 
665
+ /** Checks if a given path is ignored [ds] */
603
666
  function isPathIgnored(targetPath) {
604
- // Checks if a path is ignored [ds]
605
667
  const filename = path.basename(targetPath);
606
668
  for (const pattern of allIgnored) {
607
669
  const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
@@ -611,7 +673,7 @@ Options:
611
673
  return false;
612
674
  }
613
675
 
614
- /** Processes a path, scanning directories or analyzing files [ds] */
676
+ /** Processes a given path [ds] */
615
677
  async function processPath(targetPath) {
616
678
  const stats = fs.statSync(targetPath);
617
679
 
package/bin/setup-hook.js CHANGED
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const readline = require('readline');
5
5
 
6
- /** Installs Git hooks for auto-generating comments [ds] */
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 running in a TTY to prompt user for commenting mode [ds]
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
- const answer = await askQuestion('Select (1-3, default: 1): ');
31
- modeChoice = answer.trim() || '1';
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
- // Path to pre-commit hook script [ds]
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 content to file [ds]
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
- // Path to post-commit script [ds]
66
+ // Define the post-commit script path [ds]
58
67
  const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
59
68
 
60
- // Path to post-commit hook script [ds]
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
- // Log successful installation of post-commit hook [ds]
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
- // Run installHooks if this script is the main module [ds]
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, such as an API key, without echoing the input to the console. [ds] */
7
+ /** Prompts the user for a secret input, hiding the input from the console [ds] */
8
8
  function askSecret(query) {
9
9
  return new Promise((resolve) => {
10
10
  const rl = readline.createInterface({
@@ -14,7 +14,6 @@ function askSecret(query) {
14
14
  process.stdout.write(query);
15
15
  process.stdin.setRawMode(true);
16
16
  let key = '';
17
- // Handle key presses, including enter, backspace, and Ctrl+C [ds]
18
17
  const onData = (char) => {
19
18
  char = char.toString();
20
19
  if (char === '\n' || char === '\r') {
@@ -34,7 +33,7 @@ function askSecret(query) {
34
33
  });
35
34
  }
36
35
 
37
- /** Retrieves the configuration, either from environment variables or by prompting the user to set up a configuration file. [ds] */
36
+ /** Retrieves the configuration, either from environment variables or by prompting the user [ds] */
38
37
  async function getConfig(forceWizard = false) {
39
38
  if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
40
39
  const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
@@ -48,7 +47,7 @@ async function getConfig(forceWizard = false) {
48
47
  };
49
48
  }
50
49
 
51
- // If no configuration file exists or the forceWizard flag is set, prompt the user to set up a configuration [ds]
50
+ // If the configuration file does not exist or forceWizard is true, prompt the user to configure [ds]
52
51
  if (!fs.existsSync(configPath) || forceWizard) {
53
52
  let rl = readline.createInterface({
54
53
  input: process.stdin,
@@ -59,12 +58,13 @@ async function getConfig(forceWizard = false) {
59
58
  let config = null;
60
59
  let confirmed = false;
61
60
 
61
+ // Continuously prompt the user until the configuration is confirmed [ds]
62
62
  while (!confirmed) {
63
63
  let baseUrl = "";
64
64
  let model = "";
65
65
  let provider = "";
66
66
 
67
- // Display a menu of available AI providers [ds]
67
+ // Display the list of available AI providers [ds]
68
68
  console.log("\nWhich AI Provider Do You want to use?");
69
69
  console.log("1. Groq (Free, Fast, Llama-3)");
70
70
  console.log("2. Gemini (Free Tier)");
@@ -94,8 +94,16 @@ async function getConfig(forceWizard = false) {
94
94
  model = customModel.trim() || 'gpt-4o';
95
95
  } else if (choice === '4') {
96
96
  provider = 'custom';
97
- model = await askQuestion("Model name (e.g., llama3): ");
98
- baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
97
+ while (true) {
98
+ model = (await askQuestion("Model name (e.g., llama3): ")).trim();
99
+ if (model) break;
100
+ console.log("Model name cannot be empty.");
101
+ }
102
+ while (true) {
103
+ baseUrl = (await askQuestion("Base URL (e.g., http://localhost:11434): ")).trim();
104
+ if (baseUrl) break;
105
+ console.log("Base URL cannot be empty.");
106
+ }
99
107
  } else if (choice === '5') {
100
108
  provider = 'claude';
101
109
  baseUrl = 'https://api.anthropic.com';
@@ -107,24 +115,46 @@ async function getConfig(forceWizard = false) {
107
115
  continue;
108
116
  }
109
117
 
118
+ // Prompt the user for an API key [ds]
110
119
  let apiKey = '';
111
- // If running in a TTY, use a secure method to prompt for the API key [ds]
112
- if (process.stdin.isTTY) {
113
- rl.close();
114
- apiKey = await askSecret("Paste your API key (leave blank for local models): ");
115
-
116
- rl = readline.createInterface({
117
- input: process.stdin,
118
- output: process.stdout
119
- });
120
- askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
121
- } else {
122
- apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
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 prune existing comments [ds]
126
- const pruneAns = await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ");
127
- const autoPrune = pruneAns.toLowerCase() === 'y';
145
+ // Ask the user if they want to enable auto-pruning of existing comments [ds]
146
+ let autoPrune = false;
147
+ while (true) {
148
+ const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
149
+ if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
150
+ autoPrune = false;
151
+ break;
152
+ } else if (pruneAns === 'y' || pruneAns === 'yes') {
153
+ autoPrune = true;
154
+ break;
155
+ }
156
+ console.log("Invalid choice. Please enter 'y' or 'n'.");
157
+ }
128
158
 
129
159
  // Display a summary of the configuration [ds]
130
160
  console.log("\n--- Configuration Summary ---");
@@ -136,27 +166,31 @@ async function getConfig(forceWizard = false) {
136
166
  console.log("-----------------------------\n");
137
167
 
138
168
  // Confirm the configuration with the user [ds]
139
- const confirm = await askQuestion("Does this look correct? (y/n, default: y): ");
140
- if (confirm.toLowerCase() === 'y' || confirm.trim() === '') {
141
- config = {
142
- provider,
143
- apiKey,
144
- model,
145
- baseUrl,
146
- autoPrune
147
- };
148
- confirmed = true;
149
- } else {
150
- console.log("Let's restart the configuration setup.");
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 a file [ds]
190
+ // Write the configuration to the config file [ds]
157
191
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
158
192
  try {
159
- // Set permissions on the configuration file to prevent other users from reading it [ds]
193
+ // Set the permissions of the config file to prevent other users from reading it [ds]
160
194
  if (process.platform !== 'win32') {
161
195
  fs.chmodSync(configPath, 0o600);
162
196
  }
@@ -170,4 +204,4 @@ async function getConfig(forceWizard = false) {
170
204
  }
171
205
  }
172
206
 
173
- module.exports = { getConfig };
207
+ module.exports = { getConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "An agent-agnostic CLI tool that automatically adds JSDoc and inline comments to your code using free LLMs.",
5
5
  "author": "mwahaj36",
6
6
  "license": "MIT",