devsplain 2.2.0 → 2.2.2

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/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,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
+ // Determines if a line 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 in the given lines of code [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
+ // Splices comments into the provided data [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
  }
@@ -696,6 +744,7 @@ Options:
696
744
  --base-url <url> Override base URL for custom APIs
697
745
  --config Force run the configuration setup wizard
698
746
  --setup-hook Install Git pre-commit and post-commit hooks in repository
747
+ --remove-hook Remove devsplain-installed Git hooks from repository
699
748
  --help, -h Show this help message
700
749
  --version, -v Show version information
701
750
  `);
@@ -719,11 +768,18 @@ Options:
719
768
 
720
769
  if (args.includes('--setup-hook')) {
721
770
  rl.close();
722
- const installHooks = require('./setup-hook.js');
771
+ const { installHooks } = require('./setup-hook.js');
723
772
  await installHooks();
724
773
  return;
725
774
  }
726
775
 
776
+ if (args.includes('--remove-hook')) {
777
+ rl.close();
778
+ const { removeHooks } = require('./setup-hook.js');
779
+ await removeHooks();
780
+ return;
781
+ }
782
+
727
783
  const getArgValue = (flag) => {
728
784
  const index = args.indexOf(flag);
729
785
  if (index !== -1 && index + 1 < args.length) {
@@ -823,7 +879,6 @@ Options:
823
879
  return false;
824
880
  }
825
881
 
826
- // Processes a given file path, either a file or directory [ds]
827
882
  async function processPath(targetPath) {
828
883
  const stats = fs.statSync(targetPath);
829
884
 
@@ -881,7 +936,6 @@ Options:
881
936
  fs.renameSync(tempPath, targetPath);
882
937
  console.log(` Successfully saved ${targetPath}`);
883
938
  } else {
884
- // Asks the user if they want to save the commented code to a file [ds]
885
939
  console.log(` Skipped ${targetPath}`);
886
940
  }
887
941
  } else {
@@ -914,7 +968,6 @@ Options:
914
968
  rl.close();
915
969
  }
916
970
 
917
- // Starts the CLI application if this script is run directly [ds]
918
971
  if (require.main === module) {
919
972
  runCLI().catch(err => {
920
973
  console.error(err);
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 and configures commit settings [ds] */
6
+ // Installs Git hooks for automatic comment generation [ds]
7
7
  async function installHooks() {
8
8
  try {
9
9
  const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
@@ -14,7 +14,7 @@ async function installHooks() {
14
14
  }
15
15
 
16
16
  let modeChoice = '1';
17
- // Check if process is run in a TTY environment [ds]
17
+ // Prompt user for commenting mode if running in a TTY [ds]
18
18
  if (process.stdout.isTTY) {
19
19
  const rl = readline.createInterface({
20
20
  input: process.stdin,
@@ -22,13 +22,13 @@ async function installHooks() {
22
22
  });
23
23
  const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
24
24
 
25
- // Prompt user to select the default commenting mode [ds]
25
+ // Display commenting mode options to the user [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
30
 
31
- // Validate user input for commenting mode [ds]
31
+ // Loop until a valid mode choice is selected [ds]
32
32
  while (true) {
33
33
  const answer = (await askQuestion('Select (1-3, default: 1): ')).trim();
34
34
  if (answer === '' || ['1', '2', '3'].includes(answer)) {
@@ -40,7 +40,7 @@ async function installHooks() {
40
40
  rl.close();
41
41
  }
42
42
 
43
- // Determine mode arguments based on user choice [ds]
43
+ // Determine mode arguments based on user selection [ds]
44
44
  let modeArgs = '';
45
45
  if (modeChoice === '2') {
46
46
  modeArgs = ' --light';
@@ -48,7 +48,7 @@ async function installHooks() {
48
48
  modeArgs = ' --full';
49
49
  }
50
50
 
51
- // Define the pre-commit hook script [ds]
51
+ // Create pre-commit hook script [ds]
52
52
  const preCommitHookPath = path.join(hooksDir, 'pre-commit');
53
53
  const preCommitContent = `#!/bin/sh
54
54
  # devsplain native pre-commit hook
@@ -57,63 +57,126 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
57
57
  npm test || exit 1
58
58
  fi
59
59
  `;
60
- // Write the pre-commit hook to the Git hooks directory [ds]
60
+ // Write pre-commit hook to file [ds]
61
61
  fs.writeFileSync(preCommitHookPath, preCommitContent);
62
62
  try {
63
63
  fs.chmodSync(preCommitHookPath, 0o755);
64
64
  } catch (err) {}
65
65
 
66
- // Define the post-commit script path [ds]
66
+ // Path to post-commit script [ds]
67
67
  const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
68
68
 
69
- // Define the post-commit hook script [ds]
69
+ // Create post-commit hook script [ds]
70
70
  const postCommitHookPath = path.join(hooksDir, 'post-commit');
71
71
  const postCommitContent = `#!/bin/sh
72
72
  # devsplain native post-commit hook
73
73
  echo "Auto-generating comments for files in the last commit..."
74
74
  node "${postCommitScript}"${modeArgs} || exit 1
75
75
  `;
76
+ // Write post-commit hook to file [ds]
76
77
  fs.writeFileSync(postCommitHookPath, postCommitContent);
77
78
  try {
78
79
  fs.chmodSync(postCommitHookPath, 0o755);
79
80
  } catch (err) {}
80
81
 
81
- // Inform the user about the successful installation of the post-commit hook [ds]
82
+ // Log successful installation of post-commit hook [ds]
82
83
  console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
83
84
 
84
- // Check if a .devsplainignore file exists in the Git root directory [ds]
85
+ // Path to devsplain ignore file [ds]
85
86
  const ignorePath = path.join(gitRoot, '.devsplainignore');
87
+ const defaultIgnoreLines = [
88
+ 'node_modules/', '.git/', 'dist/', 'build/', 'out/',
89
+ '.next/', '.nuxt/', '.svelte-kit/',
90
+ 'venv/', 'env/', '.venv/',
91
+ '.vscode/', '.idea/', 'coverage/',
92
+ 'tests/', '__tests__/', 'fixtures/'
93
+ ];
94
+ // List of default patterns to ignore [ds]
95
+
96
+ const gitignorePath = path.join(gitRoot, '.gitignore');
97
+ let gitignoreLines = [];
98
+ if (fs.existsSync(gitignorePath)) {
99
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
100
+ gitignoreLines = gitignoreContent.split(/\r?\n/)
101
+ .map(l => l.trim())
102
+ .filter(l => l && !l.startsWith('#'));
103
+ }
104
+
105
+ // Check if .gitignore file exists [ds]
86
106
  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
+ const gitignoreOnly = gitignoreLines.filter(p => !defaultIgnoreLines.includes(p));
108
+ let content = defaultIgnoreLines.join('\n') + '\n';
109
+ if (gitignoreOnly.length > 0) {
110
+ content += '\n# From .gitignore\n' + gitignoreOnly.join('\n') + '\n';
111
+ }
112
+ fs.writeFileSync(ignorePath, content);
113
+ console.log(`[devsplain] Created .devsplainignore at: ${ignorePath}`);
114
+ if (gitignoreOnly.length > 0) {
115
+ console.log(`[devsplain] Merged ${gitignoreOnly.length} pattern(s) from .gitignore into .devsplainignore.`);
116
+ }
117
+ } else {
118
+ const existingContent = fs.readFileSync(ignorePath, 'utf8');
119
+ const existingLines = existingContent.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
120
+ const newPatterns = gitignoreLines.filter(p => !existingLines.includes(p));
121
+ if (newPatterns.length > 0) {
122
+ const appendContent = '\n# From .gitignore\n' + newPatterns.join('\n') + '\n';
123
+ fs.appendFileSync(ignorePath, appendContent);
124
+ console.log(`[devsplain] Merged ${newPatterns.length} new pattern(s) from .gitignore into .devsplainignore.`);
125
+ } else if (gitignoreLines.length > 0) {
126
+ console.log('[devsplain] .devsplainignore is already up-to-date with .gitignore patterns.');
127
+ }
128
+ // Handle any errors during hook installation [ds]
107
129
  }
130
+
108
131
  } catch (e) {
109
132
  console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
110
133
  console.warn(e.message);
111
134
  }
112
135
  }
113
136
 
114
- // Check if the script is run as the main module [ds]
137
+ // Removes Git hooks installed by devsplain [ds]
138
+ async function removeHooks() {
139
+ try {
140
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
141
+ const hooksDir = path.join(gitDir, 'hooks');
142
+ const hookSignatures = {
143
+ 'pre-commit': '# devsplain native pre-commit hook',
144
+ 'post-commit': '# devsplain native post-commit hook'
145
+ };
146
+
147
+ let removed = 0;
148
+ // Iterate through hook signatures to remove [ds]
149
+ for (const [hookName, signature] of Object.entries(hookSignatures)) {
150
+ const hookPath = path.join(hooksDir, hookName);
151
+ if (fs.existsSync(hookPath)) {
152
+ const content = fs.readFileSync(hookPath, 'utf8');
153
+ if (content.includes(signature)) {
154
+ fs.unlinkSync(hookPath);
155
+ console.log(`[devsplain] Removed ${hookName} hook at: ${hookPath}`);
156
+ removed++;
157
+ } else {
158
+ console.log(`[devsplain] Skipping ${hookName}: not installed by devsplain.`);
159
+ }
160
+ } else {
161
+ console.log(`[devsplain] No ${hookName} hook found at: ${hookPath}`);
162
+ }
163
+ }
164
+
165
+ // Log removal of hooks [ds]
166
+ if (removed > 0) {
167
+ console.log(`[devsplain] Successfully removed ${removed} hook(s).`);
168
+ } else {
169
+ console.log('[devsplain] No devsplain hooks were found to remove.');
170
+ }
171
+ } catch (e) {
172
+ console.warn('Warning: Could not remove Git hooks (not inside a git repository or git command missing).');
173
+ console.warn(e.message);
174
+ }
175
+ }
176
+
177
+ // Run installHooks if this script is executed directly [ds]
115
178
  if (require.main === module) {
116
179
  installHooks();
117
180
  }
118
- // Export the installHooks function for external use [ds]
119
- module.exports = installHooks;
181
+ // Export installHooks and removeHooks functions [ds]
182
+ module.exports = { installHooks, removeHooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsplain",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",