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.
@@ -0,0 +1,96 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { spliceComments } = require('./cli');
5
+
6
+ // Wrap logic in try-catch to prevent blocking the git commit process on failure
7
+ try {
8
+ const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
9
+ // Avoid infinite loops if this hook triggered the current commit
10
+ if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
11
+ process.exit(0);
12
+ }
13
+
14
+ const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
15
+ if (!changedFilesStr) {
16
+ process.exit(0);
17
+ }
18
+ const changedFiles = changedFilesStr.split(/\r?\n/);
19
+
20
+ // Define supported file extensions for auto-documentation
21
+ const validExtensions = [
22
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
23
+ '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
24
+ '.swift', '.kt', '.dart', '.sh'
25
+ ];
26
+
27
+ // Filter changed files to include only supported extensions that currently exist
28
+ const filesToComment = changedFiles.filter(file => {
29
+ const ext = path.extname(file).toLowerCase();
30
+ return validExtensions.includes(ext) && fs.existsSync(file);
31
+ });
32
+
33
+ if (filesToComment.length === 0) {
34
+ process.exit(0);
35
+ }
36
+
37
+ console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
38
+
39
+ const args = process.argv.slice(2);
40
+ let modeFlag = '';
41
+ if (args.includes('--light')) modeFlag = ' --light';
42
+ if (args.includes('--full')) modeFlag = ' --full';
43
+
44
+ let commentedAny = false;
45
+
46
+ // Iterate through each changed file to check if content actually changed
47
+ for (const file of filesToComment) {
48
+ try {
49
+ const ext = path.extname(file).toLowerCase();
50
+ // Retrieve current file content from filesystem
51
+ const contentHead = fs.readFileSync(file, 'utf8');
52
+ let contentPrev = '';
53
+ // Attempt to fetch the version of the file from the previous commit for comparison
54
+ try {
55
+ contentPrev = execSync(`git show HEAD~1:"${file}"`, {
56
+ encoding: 'utf8',
57
+ stdio: ['ignore', 'pipe', 'ignore']
58
+ });
59
+ } catch (prevErr) {
60
+ }
61
+
62
+ if (contentPrev) {
63
+ // Strip existing comments to determine if the logic changed or just documentation
64
+ const cleanHead = spliceComments(contentHead, [], 'clean', ext);
65
+ const cleanPrev = spliceComments(contentPrev, [], 'clean', ext);
66
+ if (cleanHead === cleanPrev) {
67
+ console.log(`[devsplain] Skipping ${file}: commit contains only comment changes.`);
68
+ continue;
69
+ }
70
+ }
71
+ } catch (cleanErr) {
72
+ }
73
+
74
+ console.log(`[devsplain] Automatically commenting file: ${file}`);
75
+ try {
76
+ // Invoke the CLI tool to generate comments for the changed file
77
+ execSync(`node bin/cli.js "${file}" --force${modeFlag}`, { stdio: 'inherit' });
78
+ commentedAny = true;
79
+ } catch (err) {
80
+ console.warn(`[devsplain] Warning: Failed to comment ${file}: ${err.message}`);
81
+ }
82
+ }
83
+
84
+ // If changes were made, stage and commit them automatically to the current branch
85
+ if (commentedAny) {
86
+ const status = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
87
+ if (status.length > 0) {
88
+ console.log('[devsplain] Staging and committing auto-generated comments...');
89
+ // Use --no-verify to prevent triggering this hook recursively during the commit
90
+ execSync('git commit -am "docs: auto-generated comments by devsplain" --no-verify', { stdio: 'inherit' });
91
+ console.log('[devsplain] Comments committed successfully! Rollback via: git reset --hard HEAD~1');
92
+ }
93
+ }
94
+ } catch (e) {
95
+ console.warn(`[devsplain] Warning: post-commit hook script failed: ${e.message}`);
96
+ }
@@ -0,0 +1,79 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const readline = require('readline');
5
+
6
+ /**
7
+ * Configures and installs git pre-commit and post-commit hooks.
8
+ * Detects the local git repository and prompts the user for comment mode preferences.
9
+ */
10
+ async function installHooks() {
11
+ try {
12
+ // Resolve the actual .git directory path
13
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
14
+ const hooksDir = path.join(gitDir, 'hooks');
15
+ if (!fs.existsSync(hooksDir)) {
16
+ fs.mkdirSync(hooksDir, { recursive: true });
17
+ }
18
+
19
+ let modeChoice = '1';
20
+ // Check if running in an interactive terminal to prompt for preferences
21
+ if (process.stdout.isTTY) {
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout
25
+ });
26
+ // Helper to wrap readline as a Promise for async/await control flow
27
+ const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
28
+
29
+ console.log('\nSelect default commenting mode for Git commits:');
30
+ console.log('1. Balanced (mix of JSDoc and sparse inline comments)');
31
+ console.log('2. Light (JSDoc block comments above functions only)');
32
+ console.log('3. Full (aggressive inline commenting)');
33
+ const answer = await askQuestion('Select (1-3, default: 1): ');
34
+ modeChoice = answer.trim() || '1';
35
+ rl.close();
36
+ }
37
+
38
+ // Map user selection to CLI argument flags for the post-commit handler
39
+ let modeArgs = '';
40
+ if (modeChoice === '2') {
41
+ modeArgs = ' --light';
42
+ } else if (modeChoice === '3') {
43
+ modeArgs = ' --full';
44
+ }
45
+
46
+ // Define and write the pre-commit shell script
47
+ const preCommitHookPath = path.join(hooksDir, 'pre-commit');
48
+ const preCommitContent = `#!/bin/sh
49
+ # devsplain native pre-commit hook
50
+ echo "Running pre-commit tests..."
51
+ npm test || exit 1
52
+ `;
53
+ fs.writeFileSync(preCommitHookPath, preCommitContent);
54
+ // Ensure the shell script is executable by the system
55
+ try {
56
+ fs.chmodSync(preCommitHookPath, 0o755);
57
+ } catch (err) {}
58
+
59
+ // Define and write the post-commit shell script
60
+ const postCommitHookPath = path.join(hooksDir, 'post-commit');
61
+ const postCommitContent = `#!/bin/sh
62
+ # devsplain native post-commit hook
63
+ echo "Auto-generating comments for files in the last commit..."
64
+ node bin/post-commit.js${modeArgs} || exit 1
65
+ `;
66
+ fs.writeFileSync(postCommitHookPath, postCommitContent);
67
+ // Ensure the post-commit shell script is executable
68
+ try {
69
+ fs.chmodSync(postCommitHookPath, 0o755);
70
+ } catch (err) {}
71
+
72
+ console.log('Success: Native Git pre-commit and post-commit hooks installed successfully!');
73
+ } catch (e) {
74
+ console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
75
+ }
76
+ }
77
+
78
+ // Execute the hook installation sequence
79
+ installHooks();
package/lib/config.js CHANGED
@@ -1,81 +1,125 @@
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
- const rl = readline.createInterface({
8
- input: process.stdin,
9
- output: process.stdout
10
- });
11
-
12
- /**
13
- * Asks a question via the readline interface.
14
- * @param {string} query
15
- * @returns {Promise<string>}
16
- */
17
- const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
18
-
19
- /**
20
- * Retrieves the configuration, creating a new one if it does not exist.
21
- * @returns {Promise<Object>}
22
- */
23
- async function getConfig() {
24
- if (!fs.existsSync(configPath)) {
25
- let baseUrl = "";
26
- let model = "";
27
- let provider = "";
28
-
29
- console.log("Which AI Provider Do You want to use?");
30
- console.log("1. Groq (Free, Fast, Llama-3)");
31
- console.log("2. Gemini (Free Tier)");
32
- console.log("3. OpenAI (Paid)");
33
- console.log("4. Custom (Ollama, local, etc)");
34
-
35
- const choice = await askQuestion("Select (1-4): ");
36
-
37
- if (choice === '1') {
38
- provider = 'groq';
39
- model = 'llama-3.3-70b-versatile';
40
- baseUrl = 'https://api.groq.com/openai';
41
- console.log("\nGet your free Groq key here: https://console.groq.com/keys");
42
- } else if (choice === '2') {
43
- provider = 'gemini';
44
- model = 'gemini-2.0-flash';
45
- baseUrl = null;
46
- console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
47
- } else if (choice === '3') {
48
- provider = 'openai';
49
- model = 'gpt-4o';
50
- baseUrl = 'https://api.openai.com';
51
- console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
52
- } else if (choice === '4') {
53
- provider = 'custom';
54
- model = await askQuestion("Model name (e.g., llama3): ");
55
- baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
56
- }
57
-
58
- const apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
59
-
60
- rl.close();
61
-
62
- const config = {
63
- provider: provider,
64
- apiKey: apiKey,
65
- model: model,
66
- baseUrl: baseUrl
67
- };
68
-
69
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
70
-
71
- return config;
72
- } else {
73
- rl.close();
74
-
75
- const rawData = fs.readFileSync(configPath, 'utf8');
76
-
77
- return JSON.parse(rawData);
78
- }
79
- }
80
-
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
+ /**
8
+ * Retrieves configuration from environment variables or a local config file.
9
+ * Runs a setup wizard if configuration is missing or forced.
10
+ * @param {boolean} forceWizard - Whether to force the interactive setup.
11
+ * @returns {Promise<Object>} The configuration object.
12
+ */
13
+ async function getConfig(forceWizard = false) {
14
+ // Priority 1: Check environment variables for configuration
15
+ if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
16
+ const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
17
+ const model = process.env.DEVSPLAIN_MODEL || (provider === 'gemini' ? 'gemini-2.0-flash' : 'llama-3.3-70b-versatile');
18
+ const baseUrl = process.env.DEVSPLAIN_BASE_URL || (provider === 'gemini' ? null : 'https://api.groq.com/openai');
19
+ return {
20
+ provider,
21
+ apiKey: process.env.DEVSPLAIN_API_KEY || '',
22
+ model,
23
+ baseUrl
24
+ };
25
+ }
26
+
27
+ // Priority 2: Check for existing config file or run setup wizard
28
+ if (!fs.existsSync(configPath) || forceWizard) {
29
+ const rl = readline.createInterface({
30
+ input: process.stdin,
31
+ output: process.stdout
32
+ });
33
+ // Promisify readline to allow async flow
34
+ const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
35
+
36
+ let config = null;
37
+ let confirmed = false;
38
+
39
+ // Loop until user confirms configuration choices
40
+ while (!confirmed) {
41
+ let baseUrl = "";
42
+ let model = "";
43
+ let provider = "";
44
+
45
+ console.log("\nWhich AI Provider Do You want to use?");
46
+ console.log("1. Groq (Free, Fast, Llama-3)");
47
+ console.log("2. Gemini (Free Tier)");
48
+ console.log("3. OpenAI (Paid)");
49
+ console.log("4. Custom (Ollama, local, etc)");
50
+
51
+ const choice = await askQuestion("Select (1-4): ");
52
+
53
+ // Handle specific provider setup logic
54
+ if (choice === '1') {
55
+ provider = 'groq';
56
+ baseUrl = 'https://api.groq.com/openai';
57
+ console.log("\nGet your free Groq key here: https://console.groq.com/keys");
58
+ const customModel = await askQuestion("Model name (press Enter for default 'llama-3.3-70b-versatile'): ");
59
+ model = customModel.trim() || 'llama-3.3-70b-versatile';
60
+ } else if (choice === '2') {
61
+ provider = 'gemini';
62
+ baseUrl = null;
63
+ console.log("\nGet your free Gemini key here: https://aistudio.google.com/apikey");
64
+ const customModel = await askQuestion("Model name (press Enter for default 'gemini-2.0-flash'): ");
65
+ model = customModel.trim() || 'gemini-2.0-flash';
66
+ } else if (choice === '3') {
67
+ provider = 'openai';
68
+ baseUrl = 'https://api.openai.com';
69
+ console.log("\nGet your OpenAI key here: https://platform.openai.com/api-keys");
70
+ const customModel = await askQuestion("Model name (press Enter for default 'gpt-4o'): ");
71
+ model = customModel.trim() || 'gpt-4o';
72
+ } else if (choice === '4') {
73
+ provider = 'custom';
74
+ model = await askQuestion("Model name (e.g., llama3): ");
75
+ baseUrl = await askQuestion("Base URL (e.g., http://localhost:11434): ");
76
+ } else {
77
+ console.log("Invalid choice. Please select 1, 2, 3, or 4.");
78
+ continue;
79
+ }
80
+
81
+ const apiKey = await askQuestion("Paste your API key (leave blank for local models): ");
82
+
83
+ console.log("\n--- Configuration Summary ---");
84
+ console.log(`Provider: ${provider}`);
85
+ console.log(`Model: ${model}`);
86
+ console.log(`Base URL: ${baseUrl || 'N/A'}`);
87
+ // Mask API key for security in display
88
+ console.log(`API Key: ${apiKey ? apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 4)) : 'None'}`);
89
+ console.log("-----------------------------\n");
90
+
91
+ const confirm = await askQuestion("Does this look correct? (y/n, default: y): ");
92
+ if (confirm.toLowerCase() === 'y' || confirm.trim() === '') {
93
+ config = {
94
+ provider,
95
+ apiKey,
96
+ model,
97
+ baseUrl
98
+ };
99
+ confirmed = true;
100
+ } else {
101
+ console.log("Let's restart the configuration setup.");
102
+ }
103
+ }
104
+
105
+ rl.close();
106
+
107
+ // Save finalized config to home directory
108
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
109
+ try {
110
+ // Ensure config file is not readable by other users (POSIX only)
111
+ if (process.platform !== 'win32') {
112
+ fs.chmodSync(configPath, 0o600);
113
+ }
114
+ } catch (chmodErr) {
115
+ }
116
+
117
+ return config;
118
+ } else {
119
+ // Priority 3: Load from existing file
120
+ const rawData = fs.readFileSync(configPath, 'utf8');
121
+ return JSON.parse(rawData);
122
+ }
123
+ }
124
+
81
125
  module.exports = { getConfig };