devsplain 2.1.1 → 2.2.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.
Files changed (3) hide show
  1. package/bin/cli.js +56 -11
  2. package/lib/config.js +284 -207
  3. package/package.json +1 -1
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
+ // Determines whether the current Git repository has uncommitted changes [ds]
14
14
  function isGitDirty() {
15
15
  try {
16
16
  const gitDir = execSync('git rev-parse --is-inside-work-tree', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
@@ -23,7 +23,7 @@ function isGitDirty() {
23
23
  return false;
24
24
  }
25
25
 
26
- // Checks if a line of code is inside a string literal [ds]
26
+ // Checks whether a given line of code is inside a string literal [ds]
27
27
  function isLineInsideString(lines, targetLineIndex, ext = '') {
28
28
  const isPython = ext.toLowerCase() === '.py';
29
29
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -236,7 +236,7 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
236
236
  return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
237
237
  }
238
238
 
239
- // Analyzes comments in a given set of code lines [ds]
239
+ // Analyzes comments within a given list of code lines [ds]
240
240
  function analyzeComments(lines, ext = '') {
241
241
  const isPython = ext.toLowerCase() === '.py';
242
242
  const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
@@ -477,7 +477,7 @@ function analyzeComments(lines, ext = '') {
477
477
  return analysis;
478
478
  }
479
479
 
480
- // Splices comments into the given code data [ds]
480
+ // Inserts or removes comments from code based on the specified mode [ds]
481
481
  function spliceComments(data, comments, mode = 'default', ext = '') {
482
482
  const hasCRLF = data.includes('\r\n');
483
483
  const lineEnding = hasCRLF ? '\r\n' : '\n';
@@ -530,7 +530,23 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
530
530
  if (lineAnalysis.isPureComment) {
531
531
  finalDeletions.add(lineNum);
532
532
  } else if (lineAnalysis.commentStartIndex !== -1) {
533
- annotated[i].text = lineStr.slice(0, lineAnalysis.commentStartIndex).trimEnd();
533
+ const idx = lineAnalysis.commentStartIndex;
534
+ const remainder = lineStr.slice(idx);
535
+ let newText = lineStr.slice(0, idx).trimEnd();
536
+
537
+ if (remainder.startsWith('/*')) {
538
+ const endIdx = remainder.indexOf('*/');
539
+ if (endIdx !== -1) {
540
+ newText = lineStr.slice(0, idx) + remainder.slice(endIdx + 2);
541
+ }
542
+ } else if (remainder.startsWith('<!--')) {
543
+ const endIdx = remainder.indexOf('-->');
544
+ if (endIdx !== -1) {
545
+ newText = lineStr.slice(0, idx) + remainder.slice(endIdx + 3);
546
+ }
547
+ }
548
+
549
+ annotated[i].text = newText.trimEnd();
534
550
  }
535
551
  } else if (mode === 'clean') {
536
552
  const isDsBlockLine = dsBlocks.has(lineNum);
@@ -542,7 +558,23 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
542
558
  }
543
559
  } else if (lineAnalysis.commentStartIndex !== -1) {
544
560
  if (isDsBlockLine || hasDsInline) {
545
- annotated[i].text = lineStr.slice(0, lineAnalysis.commentStartIndex).trimEnd();
561
+ const idx = lineAnalysis.commentStartIndex;
562
+ const remainder = lineStr.slice(idx);
563
+ let newText = lineStr.slice(0, idx).trimEnd();
564
+
565
+ if (remainder.startsWith('/*')) {
566
+ const endIdx = remainder.indexOf('*/');
567
+ if (endIdx !== -1) {
568
+ newText = lineStr.slice(0, idx) + remainder.slice(endIdx + 2);
569
+ }
570
+ } else if (remainder.startsWith('<!--')) {
571
+ const endIdx = remainder.indexOf('-->');
572
+ if (endIdx !== -1) {
573
+ newText = lineStr.slice(0, idx) + remainder.slice(endIdx + 3);
574
+ }
575
+ }
576
+
577
+ annotated[i].text = newText.trimEnd();
546
578
  }
547
579
  }
548
580
  }
@@ -643,7 +675,23 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
643
675
  const isDsBlockLine = dsBlocks.has(origIdx + 1);
644
676
  const hasDsInline = originalLine.includes('[ds]');
645
677
  if (mode === 'prune' || (mode === 'clean' && (hasDsInline || isDsBlockLine))) {
646
- const expectedStripped = originalLine.slice(0, lineAnalysis.commentStartIndex).trimEnd();
678
+ const idx = lineAnalysis.commentStartIndex;
679
+ const remainder = originalLine.slice(idx);
680
+ let expectedStripped = originalLine.slice(0, idx).trimEnd();
681
+
682
+ if (remainder.startsWith('/*')) {
683
+ const endIdx = remainder.indexOf('*/');
684
+ if (endIdx !== -1) {
685
+ expectedStripped = originalLine.slice(0, idx) + remainder.slice(endIdx + 2);
686
+ }
687
+ } else if (remainder.startsWith('<!--')) {
688
+ const endIdx = remainder.indexOf('-->');
689
+ if (endIdx !== -1) {
690
+ expectedStripped = originalLine.slice(0, idx) + remainder.slice(endIdx + 3);
691
+ }
692
+ }
693
+ expectedStripped = expectedStripped.trimEnd();
694
+
647
695
  if (text === expectedStripped) {
648
696
  return true;
649
697
  }
@@ -669,7 +717,7 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
669
717
  return annotated.map(line => line.text).join(lineEnding);
670
718
  }
671
719
 
672
- // Runs the CLI interface for the application [ds]
720
+ // Runs the command-line interface for the application [ds]
673
721
  async function runCLI() {
674
722
  rl = readline.createInterface({ input: process.stdin, output: process.stdout });
675
723
  askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
@@ -823,7 +871,6 @@ Options:
823
871
  return false;
824
872
  }
825
873
 
826
- // Processes a given file path, either a file or directory [ds]
827
874
  async function processPath(targetPath) {
828
875
  const stats = fs.statSync(targetPath);
829
876
 
@@ -881,7 +928,6 @@ Options:
881
928
  fs.renameSync(tempPath, targetPath);
882
929
  console.log(` Successfully saved ${targetPath}`);
883
930
  } else {
884
- // Asks the user if they want to save the commented code to a file [ds]
885
931
  console.log(` Skipped ${targetPath}`);
886
932
  }
887
933
  } else {
@@ -914,7 +960,6 @@ Options:
914
960
  rl.close();
915
961
  }
916
962
 
917
- // Starts the CLI application if this script is run directly [ds]
918
963
  if (require.main === module) {
919
964
  runCLI().catch(err => {
920
965
  console.error(err);
package/lib/config.js CHANGED
@@ -1,207 +1,284 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const readline = require('readline');
5
- const configPath = path.join(os.homedir(), '.devsplainrc');
6
-
7
- /** Asks the user for a secret input (e.g., an API key) without echoing the input to 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
- /** Attempts to fetch the configuration settings, either from environment variables or by prompting the user. [ds] */
37
- async function getConfig(forceWizard = false) {
38
- if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
39
- const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
40
- const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
41
- const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
42
- return {
43
- provider,
44
- apiKey: process.env.DEVSPLAIN_API_KEY || '',
45
- model,
46
- baseUrl
47
- };
48
- }
49
- // If the configuration file doesn't exist, or if we're forcing the wizard, let's prompt the user for configuration. [ds]
50
-
51
- if (!fs.existsSync(configPath) || forceWizard) {
52
- let rl = readline.createInterface({
53
- input: process.stdin,
54
- output: process.stdout
55
- });
56
- let askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
57
-
58
- let config = null;
59
- let confirmed = false;
60
-
61
- // Keep prompting the user until they confirm their configuration settings. [ds]
62
- while (!confirmed) {
63
- let baseUrl = "";
64
- let model = "";
65
- let provider = "";
66
-
67
- // Display the list of available AI providers to the user. [ds]
68
- console.log("\nWhich AI Provider Do You want to use?");
69
- console.log("1. Groq (Free, Fast, Llama-3)");
70
- console.log("2. Gemini (Free Tier)");
71
- console.log("3. OpenAI (Paid)");
72
- console.log("4. Custom (Ollama, local, etc)");
73
- console.log("5. Claude (Anthropic)");
74
-
75
- const choice = await askQuestion("Select (1-5): ");
76
-
77
- if (choice === '1') {
78
- provider = 'groq';
79
- baseUrl = 'https://api.groq.com/openai';
80
- console.log("\nGet your free Groq key here: https://console.groq.com/keys");
81
- const customModel = await askQuestion("Model name (press Enter for default 'llama-3.3-70b-versatile'): ");
82
- model = customModel.trim() || 'llama-3.3-70b-versatile';
83
- } else if (choice === '2') {
84
- provider = 'gemini';
85
- baseUrl = null;
86
- console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
87
- const customModel = await askQuestion("Model name (press Enter for default 'gemini-2.0-flash'): ");
88
- model = customModel.trim() || 'gemini-2.0-flash';
89
- } else if (choice === '3') {
90
- provider = 'openai';
91
- baseUrl = 'https://api.openai.com';
92
- console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
93
- const customModel = await askQuestion("Model name (press Enter for default 'gpt-4o'): ");
94
- model = customModel.trim() || 'gpt-4o';
95
- } else if (choice === '4') {
96
- provider = 'custom';
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
- }
107
- } else if (choice === '5') {
108
- provider = 'claude';
109
- baseUrl = 'https://api.anthropic.com';
110
- console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
111
- const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
112
- model = customModel.trim() || 'claude-3-5-sonnet-20240620';
113
- } else {
114
- console.log("Invalid choice. Please select 1, 2, 3, 4, or 5.");
115
- continue;
116
- }
117
-
118
- // Get the API key from the user. [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 human/AI comments. [ds]
146
- let autoPrune = false;
147
- while (true) {
148
- const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
149
- if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
150
- autoPrune = false;
151
- break;
152
- } else if (pruneAns === 'y' || pruneAns === 'yes') {
153
- autoPrune = true;
154
- break;
155
- }
156
- console.log("Invalid choice. Please enter 'y' or 'n'.");
157
- }
158
-
159
- // Display a summary of the user's configuration settings. [ds]
160
- console.log("\n--- Configuration Summary ---");
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'}`);
166
- console.log("-----------------------------\n");
167
-
168
- // Confirm the configuration settings with the user. [ds]
169
- while (true) {
170
- const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
171
- if (confirm === '' || confirm === 'y' || confirm === 'yes') {
172
- config = {
173
- provider,
174
- apiKey,
175
- model,
176
- baseUrl,
177
- autoPrune
178
- };
179
- confirmed = true;
180
- break;
181
- } else if (confirm === 'n' || confirm === 'no') {
182
- break;
183
- }
184
- console.log("Invalid choice. Please enter 'y' or 'n'.");
185
- }
186
- }
187
-
188
- rl.close();
189
-
190
- // Write the configuration settings to the config file. [ds]
191
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
192
- try {
193
- if (process.platform !== 'win32') {
194
- fs.chmodSync(configPath, 0o600);
195
- }
196
- } catch (chmodErr) {
197
- }
198
-
199
- return config;
200
- } else {
201
- const rawData = fs.readFileSync(configPath, 'utf8');
202
- return JSON.parse(rawData);
203
- }
204
- }
205
-
206
- // Export the getConfig function for use in other modules. [ds]
207
- module.exports = { getConfig };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const readline = require('readline');
5
+ const configPath = path.join(os.homedir(), '.devsplainrc');
6
+
7
+ // Asks for secret input from the user [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
+ // Migrates old config to the new format [ds]
37
+ function migrateConfig(oldConfig) {
38
+ if (oldConfig.provider && !oldConfig.providers) {
39
+ return {
40
+ activeProvider: oldConfig.provider,
41
+ providers: {
42
+ [oldConfig.provider]: {
43
+ apiKey: oldConfig.apiKey || '',
44
+ model: oldConfig.model || '',
45
+ baseUrl: oldConfig.baseUrl || null,
46
+ autoPrune: oldConfig.autoPrune || false
47
+ }
48
+ }
49
+ };
50
+ }
51
+ return oldConfig;
52
+ }
53
+
54
+ // Gets the config, either from environment variables, the config file, or by prompting the user [ds]
55
+ async function getConfig(forceWizard = false) {
56
+ if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
57
+ const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
58
+ const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : (provider === 'claude' ? 'claude-3-5-sonnet-20240620' : 'llama-3.3-70b-versatile'));
59
+ const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : (provider === 'claude' ? 'https://api.anthropic.com' : 'https://api.groq.com/openai'));
60
+ return {
61
+ provider,
62
+ apiKey: process.env.DEVSPLAIN_API_KEY || '',
63
+ model,
64
+ baseUrl
65
+ };
66
+ }
67
+
68
+ let fileConfig = null;
69
+ if (fs.existsSync(configPath)) {
70
+ try {
71
+ const rawData = fs.readFileSync(configPath, 'utf8');
72
+ fileConfig = migrateConfig(JSON.parse(rawData));
73
+ } catch (e) {
74
+ // Ignored, file might be corrupted
75
+ }
76
+ }
77
+
78
+ if (!fileConfig || forceWizard) {
79
+ let rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout
82
+ });
83
+ let askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
84
+
85
+ let config = fileConfig || { activeProvider: '', providers: {} };
86
+ let confirmed = false;
87
+
88
+ while (!confirmed) {
89
+ const savedProviders = Object.keys(config.providers);
90
+ let providerToConfig = null;
91
+ let wantToUpdate = true;
92
+ let isNewProvider = false;
93
+
94
+ if (savedProviders.length > 0) {
95
+ console.log("\nSaved Providers:");
96
+ savedProviders.forEach((p, i) => {
97
+ const isActive = config.activeProvider === p ? ' (active)' : '';
98
+ console.log(`${i + 1}. ${p}${isActive}`);
99
+ });
100
+ console.log(`\n${savedProviders.length + 1}. Add/Configure a different provider`);
101
+
102
+ const c = await askQuestion(`Select (1-${savedProviders.length + 1}): `);
103
+ const idx = parseInt(c) - 1;
104
+
105
+ if (idx >= 0 && idx < savedProviders.length) {
106
+ providerToConfig = savedProviders[idx];
107
+ const update = await askQuestion(`Do you want to update the API key or model for ${providerToConfig}? (y/N): `);
108
+ if (update.trim().toLowerCase() !== 'y' && update.trim().toLowerCase() !== 'yes') {
109
+ wantToUpdate = false;
110
+ }
111
+ } else if (idx === savedProviders.length) {
112
+ isNewProvider = true;
113
+ } else {
114
+ console.log("Invalid choice.");
115
+ continue;
116
+ }
117
+ } else {
118
+ isNewProvider = true;
119
+ }
120
+
121
+ let baseUrl = "";
122
+ let model = "";
123
+ let provider = "";
124
+ let apiKey = '';
125
+ let autoPrune = false;
126
+
127
+ if (wantToUpdate) {
128
+ if (isNewProvider || !providerToConfig) {
129
+ console.log("\nWhich AI Provider Do You want to use?");
130
+ console.log("1. Groq (Free, Fast, Llama-3)");
131
+ console.log("2. Gemini (Free Tier)");
132
+ console.log("3. OpenAI (Paid)");
133
+ console.log("4. Custom (Ollama, local, etc)");
134
+ console.log("5. Claude (Anthropic)");
135
+
136
+ const choice = await askQuestion("Select (1-5): ");
137
+
138
+ if (choice === '1') {
139
+ provider = 'groq';
140
+ baseUrl = 'https://api.groq.com/openai';
141
+ console.log("\nGet your free Groq key here: https://console.groq.com/keys");
142
+ const customModel = await askQuestion("Model name (press Enter for default 'llama-3.3-70b-versatile'): ");
143
+ model = customModel.trim() || 'llama-3.3-70b-versatile';
144
+ } else if (choice === '2') {
145
+ provider = 'gemini';
146
+ baseUrl = null;
147
+ console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
148
+ const customModel = await askQuestion("Model name (press Enter for default 'gemini-2.0-flash'): ");
149
+ model = customModel.trim() || 'gemini-2.0-flash';
150
+ } else if (choice === '3') {
151
+ provider = 'openai';
152
+ baseUrl = 'https://api.openai.com';
153
+ console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
154
+ const customModel = await askQuestion("Model name (press Enter for default 'gpt-4o'): ");
155
+ model = customModel.trim() || 'gpt-4o';
156
+ } else if (choice === '4') {
157
+ provider = 'custom';
158
+ while (true) {
159
+ model = (await askQuestion("Model name (e.g., llama3): ")).trim();
160
+ if (model) break;
161
+ console.log("Model name cannot be empty.");
162
+ }
163
+ while (true) {
164
+ baseUrl = (await askQuestion("Base URL (e.g., http://localhost:11434): ")).trim();
165
+ if (baseUrl) break;
166
+ console.log("Base URL cannot be empty.");
167
+ }
168
+ } else if (choice === '5') {
169
+ provider = 'claude';
170
+ baseUrl = 'https://api.anthropic.com';
171
+ console.log("\nGet your Anthropic key here: https://console.anthropic.com/settings/keys");
172
+ const customModel = await askQuestion("Model name (press Enter for default 'claude-3-5-sonnet-20240620'): ");
173
+ model = customModel.trim() || 'claude-3-5-sonnet-20240620';
174
+ } else {
175
+ console.log("Invalid choice. Please select 1, 2, 3, 4, or 5.");
176
+ continue;
177
+ }
178
+ } else {
179
+ provider = providerToConfig;
180
+ const old = config.providers[provider];
181
+ baseUrl = old.baseUrl;
182
+
183
+ let defaultModel = old.model;
184
+ const customModel = await askQuestion(`Model name (press Enter for default '${defaultModel}'): `);
185
+ model = customModel.trim() || defaultModel;
186
+
187
+ if (provider === 'custom') {
188
+ const customBase = await askQuestion(`Base URL (press Enter for default '${baseUrl}'): `);
189
+ baseUrl = customBase.trim() || baseUrl;
190
+ }
191
+ }
192
+
193
+ while (true) {
194
+ const promptMsg = provider === 'custom'
195
+ ? "Paste your API key (leave blank for local models): "
196
+ : "Paste your API key: ";
197
+
198
+ if (process.stdin.isTTY) {
199
+ rl.close();
200
+ apiKey = await askSecret(promptMsg);
201
+
202
+ rl = readline.createInterface({
203
+ input: process.stdin,
204
+ output: process.stdout
205
+ });
206
+ askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
207
+ } else {
208
+ apiKey = await askQuestion(promptMsg);
209
+ }
210
+
211
+ apiKey = apiKey.trim();
212
+ if (provider === 'custom' || apiKey) {
213
+ break;
214
+ }
215
+ console.log(`API key is required for provider '${provider}'.`);
216
+ }
217
+
218
+ while (true) {
219
+ const pruneAns = (await askQuestion("Do you want devsplain to aggressively prune (overwrite) existing human/AI comments? (y/n, default: n): ")).trim().toLowerCase();
220
+ if (pruneAns === '' || pruneAns === 'n' || pruneAns === 'no') {
221
+ autoPrune = false;
222
+ break;
223
+ } else if (pruneAns === 'y' || pruneAns === 'yes') {
224
+ autoPrune = true;
225
+ break;
226
+ }
227
+ console.log("Invalid choice. Please enter 'y' or 'n'.");
228
+ }
229
+
230
+ console.log("\n--- Configuration Summary ---");
231
+ console.log(`Provider: ${provider}`);
232
+ console.log(`Model: ${model}`);
233
+ console.log(`Base URL: ${baseUrl || 'N/A'}`);
234
+ console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
235
+ console.log(`Auto-Prune: ${autoPrune ? 'Yes' : 'No'}`);
236
+ console.log("-----------------------------\n");
237
+
238
+ while (true) {
239
+ const confirm = (await askQuestion("Does this look correct? (y/n, default: y): ")).trim().toLowerCase();
240
+ if (confirm === '' || confirm === 'y' || confirm === 'yes') {
241
+ config.activeProvider = provider;
242
+ config.providers[provider] = {
243
+ apiKey,
244
+ model,
245
+ baseUrl,
246
+ autoPrune
247
+ };
248
+ confirmed = true;
249
+ break;
250
+ } else if (confirm === 'n' || confirm === 'no') {
251
+ break; // Start over loop
252
+ }
253
+ console.log("Invalid choice. Please enter 'y' or 'n'.");
254
+ }
255
+ } else {
256
+ // User just selected an existing provider and didn't want to update it
257
+ config.activeProvider = providerToConfig;
258
+ console.log(`\nSwitched active provider to ${config.activeProvider}.`);
259
+ confirmed = true;
260
+ }
261
+ }
262
+
263
+ rl.close();
264
+
265
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
266
+ try {
267
+ if (process.platform !== 'win32') {
268
+ fs.chmodSync(configPath, 0o600);
269
+ }
270
+ } catch (chmodErr) {}
271
+
272
+ return {
273
+ provider: config.activeProvider,
274
+ ...config.providers[config.activeProvider]
275
+ };
276
+ } else {
277
+ return {
278
+ provider: fileConfig.activeProvider,
279
+ ...fileConfig.providers[fileConfig.activeProvider]
280
+ };
281
+ }
282
+ }
283
+
284
+ module.exports = { getConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.1.1",
3
+ "version": "2.2.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",