devsplain 2.2.1 → 2.2.3
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 +48 -11
- package/bin/post-commit.js +10 -10
- package/bin/setup-hook.js +117 -36
- package/lib/config.js +11 -4
- package/lib/llm.js +61 -19
- 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
|
-
//
|
|
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
|
-
//
|
|
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());
|
|
@@ -231,12 +231,13 @@ function isLineInsideString(lines, targetLineIndex, ext = '') {
|
|
|
231
231
|
if (resetsAtLineEnd) {
|
|
232
232
|
inSingle = false;
|
|
233
233
|
inDouble = false;
|
|
234
|
+
inRegex = false;
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
return inBacktick || inTripleDouble || inTripleSingle || inSingle || inDouble || inCppRawString || inRegex;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
// Analyzes comments
|
|
240
|
+
// Analyzes comments in the given lines of code [ds]
|
|
240
241
|
function analyzeComments(lines, ext = '') {
|
|
241
242
|
const isPython = ext.toLowerCase() === '.py';
|
|
242
243
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext.toLowerCase());
|
|
@@ -455,6 +456,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
455
456
|
if (resetsAtLineEnd) {
|
|
456
457
|
inSingle = false;
|
|
457
458
|
inDouble = false;
|
|
459
|
+
inRegex = false;
|
|
458
460
|
}
|
|
459
461
|
const isEntirelyInsideBlock = isInsideBlockStart && (inBlockJS || inBlockHTML || (commentStartIndex === -1));
|
|
460
462
|
let isPureComment = false;
|
|
@@ -477,7 +479,7 @@ function analyzeComments(lines, ext = '') {
|
|
|
477
479
|
return analysis;
|
|
478
480
|
}
|
|
479
481
|
|
|
480
|
-
//
|
|
482
|
+
// Splices comments into the provided data [ds]
|
|
481
483
|
function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
482
484
|
const hasCRLF = data.includes('\r\n');
|
|
483
485
|
const lineEnding = hasCRLF ? '\r\n' : '\n';
|
|
@@ -663,6 +665,33 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
663
665
|
const filteredText = filtered.map(line => line.text);
|
|
664
666
|
const filteredIndices = filtered.map(line => line.originalIndex);
|
|
665
667
|
|
|
668
|
+
// Validate that all inserted lines (originalIndex === -1) are valid comments or empty lines [ds]
|
|
669
|
+
const insertedLines = annotated.filter(line => line.originalIndex === -1);
|
|
670
|
+
let inInsertedBlock = false;
|
|
671
|
+
for (const item of insertedLines) {
|
|
672
|
+
const trimmed = item.text.trim();
|
|
673
|
+
if (!trimmed) continue;
|
|
674
|
+
if (inInsertedBlock) {
|
|
675
|
+
if (trimmed.includes('*/') || trimmed.includes('-->')) {
|
|
676
|
+
inInsertedBlock = false;
|
|
677
|
+
}
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const isValidComment =
|
|
681
|
+
trimmed.startsWith('//') ||
|
|
682
|
+
trimmed.startsWith('/*') ||
|
|
683
|
+
trimmed.startsWith('*') ||
|
|
684
|
+
trimmed.startsWith('#') ||
|
|
685
|
+
trimmed.startsWith('<!--') ||
|
|
686
|
+
trimmed.startsWith('--');
|
|
687
|
+
if (!isValidComment) {
|
|
688
|
+
throw new Error(`Safety Assertion Failed: Refused to insert non-comment code: "${trimmed}"`);
|
|
689
|
+
}
|
|
690
|
+
if ((trimmed.startsWith('/*') && !trimmed.includes('*/')) || (trimmed.startsWith('<!--') && !trimmed.includes('-->'))) {
|
|
691
|
+
inInsertedBlock = true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
666
695
|
const textEqual = filteredText.every((text, idx) => {
|
|
667
696
|
const origIdx = filteredIndices[idx];
|
|
668
697
|
const originalLine = originalLines[origIdx];
|
|
@@ -710,14 +739,13 @@ function spliceComments(data, comments, mode = 'default', ext = '') {
|
|
|
710
739
|
}
|
|
711
740
|
|
|
712
741
|
if (!textEqual || !indicesSequential) {
|
|
713
|
-
|
|
714
|
-
process.exit(1);
|
|
742
|
+
throw new Error("Safety Assertion Failed: Spliced code does not match original code minus comments!");
|
|
715
743
|
}
|
|
716
744
|
|
|
717
745
|
return annotated.map(line => line.text).join(lineEnding);
|
|
718
746
|
}
|
|
719
747
|
|
|
720
|
-
// Runs the
|
|
748
|
+
// Runs the CLI interface for the application [ds]
|
|
721
749
|
async function runCLI() {
|
|
722
750
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
723
751
|
askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
@@ -744,6 +772,7 @@ Options:
|
|
|
744
772
|
--base-url <url> Override base URL for custom APIs
|
|
745
773
|
--config Force run the configuration setup wizard
|
|
746
774
|
--setup-hook Install Git pre-commit and post-commit hooks in repository
|
|
775
|
+
--remove-hook Remove devsplain-installed Git hooks from repository
|
|
747
776
|
--help, -h Show this help message
|
|
748
777
|
--version, -v Show version information
|
|
749
778
|
`);
|
|
@@ -767,11 +796,18 @@ Options:
|
|
|
767
796
|
|
|
768
797
|
if (args.includes('--setup-hook')) {
|
|
769
798
|
rl.close();
|
|
770
|
-
const installHooks = require('./setup-hook.js');
|
|
799
|
+
const { installHooks } = require('./setup-hook.js');
|
|
771
800
|
await installHooks();
|
|
772
801
|
return;
|
|
773
802
|
}
|
|
774
803
|
|
|
804
|
+
if (args.includes('--remove-hook')) {
|
|
805
|
+
rl.close();
|
|
806
|
+
const { removeHooks } = require('./setup-hook.js');
|
|
807
|
+
await removeHooks();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
775
811
|
const getArgValue = (flag) => {
|
|
776
812
|
const index = args.indexOf(flag);
|
|
777
813
|
if (index !== -1 && index + 1 < args.length) {
|
|
@@ -810,7 +846,7 @@ Options:
|
|
|
810
846
|
const hasOverwriteFlag = args.includes('--overwrite');
|
|
811
847
|
const hasKeepFlag = args.includes('--keep');
|
|
812
848
|
|
|
813
|
-
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
|
|
849
|
+
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce && !isDryRun) {
|
|
814
850
|
console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
|
|
815
851
|
rl.close();
|
|
816
852
|
process.exit(1);
|
|
@@ -863,9 +899,10 @@ Options:
|
|
|
863
899
|
|
|
864
900
|
function isPathIgnored(targetPath) {
|
|
865
901
|
const filename = path.basename(targetPath);
|
|
902
|
+
const relPath = path.relative(process.cwd(), targetPath).replace(/\\/g, '/');
|
|
866
903
|
for (const pattern of allIgnored) {
|
|
867
904
|
const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
|
|
868
|
-
if (filename === cleanPattern) return true;
|
|
905
|
+
if (filename === cleanPattern || relPath === cleanPattern || relPath.startsWith(cleanPattern + '/')) return true;
|
|
869
906
|
if (pattern.startsWith('*') && filename.endsWith(pattern.slice(1))) return true;
|
|
870
907
|
}
|
|
871
908
|
return false;
|
|
@@ -891,7 +928,7 @@ Options:
|
|
|
891
928
|
const validExtensions = [
|
|
892
929
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
893
930
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
894
|
-
'.swift', '.kt', '.dart', '.sh'
|
|
931
|
+
'.swift', '.kt', '.dart', '.sh', '.sql'
|
|
895
932
|
];
|
|
896
933
|
|
|
897
934
|
if (!validExtensions.includes(ext)) {
|
package/bin/post-commit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
1
|
+
const { execSync, execFileSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spliceComments } = require('./cli');
|
|
@@ -30,7 +30,7 @@ try {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Get a list of changed files in the last commit [ds]
|
|
33
|
-
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
|
|
33
|
+
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r --root HEAD', { encoding: 'utf8' }).trim();
|
|
34
34
|
if (!changedFilesStr) {
|
|
35
35
|
process.exit(0);
|
|
36
36
|
}
|
|
@@ -40,7 +40,7 @@ try {
|
|
|
40
40
|
const validExtensions = [
|
|
41
41
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
42
42
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
43
|
-
'.swift', '.kt', '.dart', '.sh'
|
|
43
|
+
'.swift', '.kt', '.dart', '.sh', '.sql'
|
|
44
44
|
];
|
|
45
45
|
|
|
46
46
|
/** Filter function to determine which files to auto-comment [ds] */
|
|
@@ -97,13 +97,13 @@ try {
|
|
|
97
97
|
// Log and attempt to auto-comment the current file [ds]
|
|
98
98
|
console.log(`[devsplain] Automatically commenting file: ${file}`);
|
|
99
99
|
try {
|
|
100
|
-
let extraFlags = '';
|
|
101
|
-
if (process.env.DS_OVER) extraFlags += ' --overwrite';
|
|
102
|
-
if (process.env.DS_KEEP) extraFlags += ' --keep';
|
|
103
|
-
|
|
104
|
-
// Construct the command to run the auto-commenting CLI [ds]
|
|
105
100
|
const cliPath = path.join(__dirname, 'cli.js');
|
|
106
|
-
|
|
101
|
+
const cliArgs = [cliPath, file, '--force'];
|
|
102
|
+
if (modeFlag.trim()) cliArgs.push(modeFlag.trim());
|
|
103
|
+
if (process.env.DS_OVER) cliArgs.push('--overwrite');
|
|
104
|
+
if (process.env.DS_KEEP) cliArgs.push('--keep');
|
|
105
|
+
|
|
106
|
+
execFileSync(process.execPath, cliArgs, { stdio: 'inherit' });
|
|
107
107
|
commentedAny = true;
|
|
108
108
|
successfullyCommentedFiles.push(file);
|
|
109
109
|
} catch (err) {
|
|
@@ -115,7 +115,7 @@ try {
|
|
|
115
115
|
if (commentedAny) {
|
|
116
116
|
for (const file of successfullyCommentedFiles) {
|
|
117
117
|
try {
|
|
118
|
-
|
|
118
|
+
execFileSync('git', ['add', file]);
|
|
119
119
|
} catch (addErr) {}
|
|
120
120
|
}
|
|
121
121
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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,144 @@ 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
|
|
61
|
-
fs.
|
|
60
|
+
// Write pre-commit hook safely [ds]
|
|
61
|
+
if (fs.existsSync(preCommitHookPath)) {
|
|
62
|
+
const existing = fs.readFileSync(preCommitHookPath, 'utf8');
|
|
63
|
+
if (!existing.includes('# devsplain native pre-commit hook')) {
|
|
64
|
+
fs.appendFileSync(preCommitHookPath, '\n' + preCommitContent);
|
|
65
|
+
} else {
|
|
66
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
70
|
+
}
|
|
62
71
|
try {
|
|
63
72
|
fs.chmodSync(preCommitHookPath, 0o755);
|
|
64
73
|
} catch (err) {}
|
|
65
74
|
|
|
66
|
-
//
|
|
75
|
+
// Path to post-commit script [ds]
|
|
67
76
|
const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
|
|
68
77
|
|
|
69
|
-
//
|
|
78
|
+
// Create post-commit hook script [ds]
|
|
70
79
|
const postCommitHookPath = path.join(hooksDir, 'post-commit');
|
|
71
80
|
const postCommitContent = `#!/bin/sh
|
|
72
81
|
# devsplain native post-commit hook
|
|
73
82
|
echo "Auto-generating comments for files in the last commit..."
|
|
74
83
|
node "${postCommitScript}"${modeArgs} || exit 1
|
|
75
84
|
`;
|
|
76
|
-
|
|
85
|
+
// Write post-commit hook safely [ds]
|
|
86
|
+
if (fs.existsSync(postCommitHookPath)) {
|
|
87
|
+
const existing = fs.readFileSync(postCommitHookPath, 'utf8');
|
|
88
|
+
if (!existing.includes('# devsplain native post-commit hook')) {
|
|
89
|
+
fs.appendFileSync(postCommitHookPath, '\n' + postCommitContent);
|
|
90
|
+
} else {
|
|
91
|
+
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
95
|
+
}
|
|
77
96
|
try {
|
|
78
97
|
fs.chmodSync(postCommitHookPath, 0o755);
|
|
79
98
|
} catch (err) {}
|
|
80
99
|
|
|
81
|
-
//
|
|
100
|
+
// Log successful installation of post-commit hook [ds]
|
|
82
101
|
console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
|
|
83
102
|
|
|
84
|
-
//
|
|
103
|
+
// Path to devsplain ignore file [ds]
|
|
85
104
|
const ignorePath = path.join(gitRoot, '.devsplainignore');
|
|
105
|
+
const defaultIgnoreLines = [
|
|
106
|
+
'node_modules/', '.git/', 'dist/', 'build/', 'out/',
|
|
107
|
+
'.next/', '.nuxt/', '.svelte-kit/',
|
|
108
|
+
'venv/', 'env/', '.venv/',
|
|
109
|
+
'.vscode/', '.idea/', 'coverage/',
|
|
110
|
+
'tests/', '__tests__/', 'fixtures/'
|
|
111
|
+
];
|
|
112
|
+
// List of default patterns to ignore [ds]
|
|
113
|
+
|
|
114
|
+
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
115
|
+
let gitignoreLines = [];
|
|
116
|
+
if (fs.existsSync(gitignorePath)) {
|
|
117
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
118
|
+
gitignoreLines = gitignoreContent.split(/\r?\n/)
|
|
119
|
+
.map(l => l.trim())
|
|
120
|
+
.filter(l => l && !l.startsWith('#'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if .gitignore file exists [ds]
|
|
86
124
|
if (!fs.existsSync(ignorePath)) {
|
|
87
|
-
const
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
const gitignoreOnly = gitignoreLines.filter(p => !defaultIgnoreLines.includes(p));
|
|
126
|
+
let content = defaultIgnoreLines.join('\n') + '\n';
|
|
127
|
+
if (gitignoreOnly.length > 0) {
|
|
128
|
+
content += '\n# From .gitignore\n' + gitignoreOnly.join('\n') + '\n';
|
|
129
|
+
}
|
|
130
|
+
fs.writeFileSync(ignorePath, content);
|
|
131
|
+
console.log(`[devsplain] Created .devsplainignore at: ${ignorePath}`);
|
|
132
|
+
if (gitignoreOnly.length > 0) {
|
|
133
|
+
console.log(`[devsplain] Merged ${gitignoreOnly.length} pattern(s) from .gitignore into .devsplainignore.`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
const existingContent = fs.readFileSync(ignorePath, 'utf8');
|
|
137
|
+
const existingLines = existingContent.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
138
|
+
const newPatterns = gitignoreLines.filter(p => !existingLines.includes(p));
|
|
139
|
+
if (newPatterns.length > 0) {
|
|
140
|
+
const appendContent = '\n# From .gitignore\n' + newPatterns.join('\n') + '\n';
|
|
141
|
+
fs.appendFileSync(ignorePath, appendContent);
|
|
142
|
+
console.log(`[devsplain] Merged ${newPatterns.length} new pattern(s) from .gitignore into .devsplainignore.`);
|
|
143
|
+
} else if (gitignoreLines.length > 0) {
|
|
144
|
+
console.log('[devsplain] .devsplainignore is already up-to-date with .gitignore patterns.');
|
|
145
|
+
}
|
|
146
|
+
// Handle any errors during hook installation [ds]
|
|
107
147
|
}
|
|
148
|
+
|
|
108
149
|
} catch (e) {
|
|
109
150
|
console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
|
|
110
151
|
console.warn(e.message);
|
|
111
152
|
}
|
|
112
153
|
}
|
|
113
154
|
|
|
114
|
-
//
|
|
155
|
+
// Removes Git hooks installed by devsplain [ds]
|
|
156
|
+
async function removeHooks() {
|
|
157
|
+
try {
|
|
158
|
+
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
159
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
160
|
+
const hookSignatures = {
|
|
161
|
+
'pre-commit': '# devsplain native pre-commit hook',
|
|
162
|
+
'post-commit': '# devsplain native post-commit hook'
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let removed = 0;
|
|
166
|
+
// Iterate through hook signatures to remove [ds]
|
|
167
|
+
for (const [hookName, signature] of Object.entries(hookSignatures)) {
|
|
168
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
169
|
+
if (fs.existsSync(hookPath)) {
|
|
170
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
171
|
+
if (content.includes(signature)) {
|
|
172
|
+
fs.unlinkSync(hookPath);
|
|
173
|
+
console.log(`[devsplain] Removed ${hookName} hook at: ${hookPath}`);
|
|
174
|
+
removed++;
|
|
175
|
+
} else {
|
|
176
|
+
console.log(`[devsplain] Skipping ${hookName}: not installed by devsplain.`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`[devsplain] No ${hookName} hook found at: ${hookPath}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Log removal of hooks [ds]
|
|
184
|
+
if (removed > 0) {
|
|
185
|
+
console.log(`[devsplain] Successfully removed ${removed} hook(s).`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log('[devsplain] No devsplain hooks were found to remove.');
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.warn('Warning: Could not remove Git hooks (not inside a git repository or git command missing).');
|
|
191
|
+
console.warn(e.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Run installHooks if this script is executed directly [ds]
|
|
115
196
|
if (require.main === module) {
|
|
116
197
|
installHooks();
|
|
117
198
|
}
|
|
118
|
-
// Export
|
|
119
|
-
module.exports = installHooks;
|
|
199
|
+
// Export installHooks and removeHooks functions [ds]
|
|
200
|
+
module.exports = { installHooks, removeHooks };
|
package/lib/config.js
CHANGED
|
@@ -22,8 +22,14 @@ function askSecret(query) {
|
|
|
22
22
|
process.stdout.write('\n');
|
|
23
23
|
rl.close();
|
|
24
24
|
resolve(key);
|
|
25
|
+
} else if (char === '\u0008' || char === '\u007f') { // Backspace
|
|
26
|
+
if (key.length > 0) {
|
|
27
|
+
key = key.slice(0, -1);
|
|
28
|
+
process.stdout.write('\b \b');
|
|
29
|
+
}
|
|
25
30
|
} else if (char === '\u0003') { // Ctrl+C
|
|
26
|
-
process.
|
|
31
|
+
process.stdin.setRawMode(false);
|
|
32
|
+
process.exit(130);
|
|
27
33
|
} else {
|
|
28
34
|
key += char;
|
|
29
35
|
process.stdout.write('*');
|
|
@@ -35,7 +41,7 @@ function askSecret(query) {
|
|
|
35
41
|
|
|
36
42
|
// Migrates old config to the new format [ds]
|
|
37
43
|
function migrateConfig(oldConfig) {
|
|
38
|
-
if (oldConfig.provider && !oldConfig.providers) {
|
|
44
|
+
if (oldConfig && typeof oldConfig === 'object' && oldConfig.provider && !oldConfig.providers) {
|
|
39
45
|
return {
|
|
40
46
|
activeProvider: oldConfig.provider,
|
|
41
47
|
providers: {
|
|
@@ -75,7 +81,7 @@ async function getConfig(forceWizard = false) {
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
if (!fileConfig || forceWizard) {
|
|
84
|
+
if (!fileConfig || !fileConfig.activeProvider || !fileConfig.providers || !fileConfig.providers[fileConfig.activeProvider] || forceWizard) {
|
|
79
85
|
let rl = readline.createInterface({
|
|
80
86
|
input: process.stdin,
|
|
81
87
|
output: process.stdout
|
|
@@ -274,9 +280,10 @@ async function getConfig(forceWizard = false) {
|
|
|
274
280
|
...config.providers[config.activeProvider]
|
|
275
281
|
};
|
|
276
282
|
} else {
|
|
283
|
+
const active = fileConfig.providers[fileConfig.activeProvider] || {};
|
|
277
284
|
return {
|
|
278
285
|
provider: fileConfig.activeProvider,
|
|
279
|
-
...
|
|
286
|
+
...active
|
|
280
287
|
};
|
|
281
288
|
}
|
|
282
289
|
}
|
package/lib/llm.js
CHANGED
|
@@ -4,13 +4,16 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
|
|
|
4
4
|
// Loop through each attempt, up to the maximum number of retries [ds]
|
|
5
5
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
6
6
|
const controller = new AbortController();
|
|
7
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
7
|
+
const timeoutId = setTimeout(() => controller.abort(), 45000);
|
|
8
8
|
try {
|
|
9
9
|
const response = await fetch(url, {
|
|
10
10
|
...options,
|
|
11
11
|
signal: controller.signal
|
|
12
12
|
});
|
|
13
13
|
clearTimeout(timeoutId);
|
|
14
|
+
if (!response) {
|
|
15
|
+
throw new Error("No response received from fetch");
|
|
16
|
+
}
|
|
14
17
|
if (response.ok) {
|
|
15
18
|
return response;
|
|
16
19
|
}
|
|
@@ -22,7 +25,7 @@ async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000)
|
|
|
22
25
|
} catch (err) {
|
|
23
26
|
clearTimeout(timeoutId);
|
|
24
27
|
if (err.name === 'AbortError') {
|
|
25
|
-
lastError = new Error("Request timed out after
|
|
28
|
+
lastError = new Error("Request timed out after 45 seconds");
|
|
26
29
|
} else {
|
|
27
30
|
lastError = err;
|
|
28
31
|
}
|
|
@@ -46,8 +49,9 @@ async function getComments(code, language, config, mode = 'default') {
|
|
|
46
49
|
const extMatch = language.match(/\.[0-9a-z]+$/i);
|
|
47
50
|
const ext = extMatch ? extMatch[0].toLowerCase() : '';
|
|
48
51
|
const isPython = ext === '.py';
|
|
49
|
-
const isRubyOrShell = ['.rb', '.sh'
|
|
52
|
+
const isRubyOrShell = ['.rb', '.sh'].includes(ext);
|
|
50
53
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
|
|
54
|
+
const isCss = ['.css', '.scss'].includes(ext);
|
|
51
55
|
const isSql = ext === '.sql';
|
|
52
56
|
|
|
53
57
|
// Define the single-line comment token and examples [ds]
|
|
@@ -63,6 +67,10 @@ async function getComments(code, language, config, mode = 'default') {
|
|
|
63
67
|
singleLineToken = '<!--';
|
|
64
68
|
blockExample = '<!-- Calculates the total price -->';
|
|
65
69
|
inlineExample = '<!-- Check for null values -->';
|
|
70
|
+
} else if (isCss) {
|
|
71
|
+
singleLineToken = '/*';
|
|
72
|
+
blockExample = '/* Calculates the total price */';
|
|
73
|
+
inlineExample = '/* Check for null values */';
|
|
66
74
|
} else if (isSql) {
|
|
67
75
|
singleLineToken = '--';
|
|
68
76
|
blockExample = '-- Calculates the total price';
|
|
@@ -77,6 +85,11 @@ async function getComments(code, language, config, mode = 'default') {
|
|
|
77
85
|
instruction = `Provide highly detailed block comments above functions, and exhaustive step-by-step inline comments explaining every conditional branch, loop, variable assignment, and logical block inside function bodies. Do not be sparse; explain the code's execution flow in detail.`;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
let rule5 = `5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.`;
|
|
89
|
+
if (isCss) {
|
|
90
|
+
rule5 = `5. IMPORTANT: In CSS/SCSS, you MUST use /* ... */ for comments. DO NOT use // comments under any circumstances.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
let prompt = `
|
|
81
94
|
You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
|
|
82
95
|
${instruction}
|
|
@@ -86,7 +99,7 @@ CRITICAL RULES:
|
|
|
86
99
|
2. Each object must have exactly two properties: "line" (the integer line number where the comment should be inserted ABOVE) and "comment" (the text of the comment itself).
|
|
87
100
|
3. Do NOT include the original code in your response.
|
|
88
101
|
4. If no comments are needed, return an empty array: [].
|
|
89
|
-
|
|
102
|
+
${rule5}
|
|
90
103
|
|
|
91
104
|
Example Output:
|
|
92
105
|
[
|
|
@@ -116,10 +129,15 @@ ${numberedCode}
|
|
|
116
129
|
});
|
|
117
130
|
data = await response.json();
|
|
118
131
|
} catch (error) {
|
|
119
|
-
throw new Error(
|
|
132
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
120
133
|
}
|
|
121
134
|
if (data.error) {
|
|
122
|
-
|
|
135
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
136
|
+
throw new Error(`API Error: ${msg}`);
|
|
137
|
+
}
|
|
138
|
+
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts[0]) {
|
|
139
|
+
const reason = data.candidates?.[0]?.finishReason || 'Unknown error';
|
|
140
|
+
throw new Error(`AI Provider returned no content (finish reason: ${reason})`);
|
|
123
141
|
}
|
|
124
142
|
textResponse = data.candidates[0].content.parts[0].text;
|
|
125
143
|
} else if (config.provider === 'claude') {
|
|
@@ -145,10 +163,14 @@ ${numberedCode}
|
|
|
145
163
|
});
|
|
146
164
|
data = await response.json();
|
|
147
165
|
} catch (error) {
|
|
148
|
-
throw new Error(
|
|
166
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
149
167
|
}
|
|
150
168
|
if (data.error) {
|
|
151
|
-
|
|
169
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
170
|
+
throw new Error(`API Error: ${msg}`);
|
|
171
|
+
}
|
|
172
|
+
if (!data.content || !data.content[0] || typeof data.content[0].text !== 'string') {
|
|
173
|
+
throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
|
|
152
174
|
}
|
|
153
175
|
textResponse = data.content[0].text;
|
|
154
176
|
}
|
|
@@ -166,6 +188,7 @@ ${numberedCode}
|
|
|
166
188
|
},
|
|
167
189
|
body: JSON.stringify({
|
|
168
190
|
"model": config.model,
|
|
191
|
+
"max_tokens": 8192,
|
|
169
192
|
"messages": [{
|
|
170
193
|
"role": "user",
|
|
171
194
|
"content": prompt
|
|
@@ -174,10 +197,14 @@ ${numberedCode}
|
|
|
174
197
|
});
|
|
175
198
|
data = await response.json();
|
|
176
199
|
} catch (error) {
|
|
177
|
-
throw new Error(
|
|
200
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
178
201
|
}
|
|
179
202
|
if (data.error) {
|
|
180
|
-
|
|
203
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
204
|
+
throw new Error(`API Error: ${msg}`);
|
|
205
|
+
}
|
|
206
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || typeof data.choices[0].message.content !== 'string') {
|
|
207
|
+
throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
|
|
181
208
|
}
|
|
182
209
|
textResponse = data.choices[0].message.content;
|
|
183
210
|
}
|
|
@@ -220,15 +247,30 @@ ${numberedCode}
|
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
const trimmedComment = item.comment.trim();
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
250
|
+
const commentLines = trimmedComment.split(/\r?\n/);
|
|
251
|
+
let inBlock = false;
|
|
252
|
+
for (const cl of commentLines) {
|
|
253
|
+
const tcl = cl.trim();
|
|
254
|
+
if (!tcl) continue;
|
|
255
|
+
if (inBlock) {
|
|
256
|
+
if (tcl.includes('*/') || tcl.includes('-->')) {
|
|
257
|
+
inBlock = false;
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const startsWithMarker =
|
|
262
|
+
tcl.startsWith('//') ||
|
|
263
|
+
tcl.startsWith('/*') ||
|
|
264
|
+
tcl.startsWith('*') ||
|
|
265
|
+
tcl.startsWith('#') ||
|
|
266
|
+
tcl.startsWith('<!--') ||
|
|
267
|
+
tcl.startsWith('--');
|
|
268
|
+
if (!startsWithMarker) {
|
|
269
|
+
throw new Error(`Security Error: Comment on line ${item.line} contains invalid non-comment line: "${tcl}"`);
|
|
270
|
+
}
|
|
271
|
+
if ((tcl.startsWith('/*') && !tcl.includes('*/')) || (tcl.startsWith('<!--') && !tcl.includes('-->'))) {
|
|
272
|
+
inBlock = true;
|
|
273
|
+
}
|
|
232
274
|
}
|
|
233
275
|
}
|
|
234
276
|
}
|
package/package.json
CHANGED