devsplain 2.2.2 → 2.2.4
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 +34 -5
- package/bin/post-commit.js +10 -27
- package/bin/setup-hook.js +129 -23
- package/lib/config.js +16 -13
- package/lib/llm.js +233 -40
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -231,6 +231,7 @@ 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;
|
|
@@ -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;
|
|
@@ -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,8 +739,7 @@ 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);
|
|
@@ -818,7 +846,7 @@ Options:
|
|
|
818
846
|
const hasOverwriteFlag = args.includes('--overwrite');
|
|
819
847
|
const hasKeepFlag = args.includes('--keep');
|
|
820
848
|
|
|
821
|
-
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce) {
|
|
849
|
+
if (process.env.NODE_ENV !== 'test' && isGitDirty() && !isForce && !isDryRun) {
|
|
822
850
|
console.error("Error: Git working tree is dirty. Please commit or stash your changes, or use --force to bypass this check.");
|
|
823
851
|
rl.close();
|
|
824
852
|
process.exit(1);
|
|
@@ -871,9 +899,10 @@ Options:
|
|
|
871
899
|
|
|
872
900
|
function isPathIgnored(targetPath) {
|
|
873
901
|
const filename = path.basename(targetPath);
|
|
902
|
+
const relPath = path.relative(process.cwd(), targetPath).replace(/\\/g, '/');
|
|
874
903
|
for (const pattern of allIgnored) {
|
|
875
904
|
const cleanPattern = pattern.replace(/\/$/, '').replace(/\\$/, '');
|
|
876
|
-
if (filename === cleanPattern) return true;
|
|
905
|
+
if (filename === cleanPattern || relPath === cleanPattern || relPath.startsWith(cleanPattern + '/')) return true;
|
|
877
906
|
if (pattern.startsWith('*') && filename.endsWith(pattern.slice(1))) return true;
|
|
878
907
|
}
|
|
879
908
|
return false;
|
|
@@ -899,7 +928,7 @@ Options:
|
|
|
899
928
|
const validExtensions = [
|
|
900
929
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
901
930
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
902
|
-
'.swift', '.kt', '.dart', '.sh'
|
|
931
|
+
'.swift', '.kt', '.dart', '.sh', '.sql'
|
|
903
932
|
];
|
|
904
933
|
|
|
905
934
|
if (!validExtensions.includes(ext)) {
|
package/bin/post-commit.js
CHANGED
|
@@ -1,49 +1,41 @@
|
|
|
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');
|
|
5
5
|
|
|
6
|
-
/** Main execution block for post-commit hook [ds] */
|
|
7
6
|
try {
|
|
8
|
-
// Check if SKIP_DEVSPLAIN environment variable is set [ds]
|
|
9
7
|
if (process.env.SKIP_DEVSPLAIN) {
|
|
10
8
|
console.log('[devsplain] SKIP_DEVSPLAIN is set. Bypassing AI generation.');
|
|
11
9
|
process.exit(0);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
// Determine if Git is in a rebasing, merging, or cherry-picking state [ds]
|
|
15
12
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
16
13
|
const isRebasing = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
17
14
|
const isMerging = fs.existsSync(path.join(gitDir, 'MERGE_HEAD'));
|
|
18
15
|
const isCherryPicking = fs.existsSync(path.join(gitDir, 'CHERRY_PICK_HEAD'));
|
|
19
16
|
|
|
20
|
-
// Exit if Git is in a rebasing, merging, or cherry-picking state to avoid history conflicts [ds]
|
|
21
17
|
if (isRebasing || isMerging || isCherryPicking) {
|
|
22
18
|
console.log('[devsplain] Skipping AI comment generation during git rebase/merge/cherry-pick to avoid history conflicts.');
|
|
23
19
|
process.exit(0);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
// Retrieve the last commit message [ds]
|
|
27
22
|
const lastCommitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
|
|
28
23
|
if (lastCommitMsg === 'docs: auto-generated comments by devsplain') {
|
|
29
24
|
process.exit(0);
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { encoding: 'utf8' }).trim();
|
|
27
|
+
const changedFilesStr = execSync('git diff-tree --no-commit-id --name-only -r --root HEAD', { encoding: 'utf8' }).trim();
|
|
34
28
|
if (!changedFilesStr) {
|
|
35
29
|
process.exit(0);
|
|
36
30
|
}
|
|
37
31
|
const changedFiles = changedFilesStr.split(/\r?\n/);
|
|
38
32
|
|
|
39
|
-
/** List of valid file extensions for auto-commenting [ds] */
|
|
40
33
|
const validExtensions = [
|
|
41
34
|
'.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
|
|
42
35
|
'.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
|
|
43
|
-
'.swift', '.kt', '.dart', '.sh'
|
|
36
|
+
'.swift', '.kt', '.dart', '.sh', '.sql'
|
|
44
37
|
];
|
|
45
38
|
|
|
46
|
-
/** Filter function to determine which files to auto-comment [ds] */
|
|
47
39
|
const filesToComment = changedFiles.filter(file => {
|
|
48
40
|
const ext = path.extname(file).toLowerCase();
|
|
49
41
|
const isIgnored = file.includes('node_modules/') || file.includes('tests/') || file.includes('__tests__/') || file.includes('fixtures/');
|
|
@@ -54,10 +46,8 @@ try {
|
|
|
54
46
|
process.exit(0);
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
// Log the number of files found for auto-commenting [ds]
|
|
58
49
|
console.log(`[devsplain] Found ${filesToComment.length} file(s) in the last commit to auto-comment.`);
|
|
59
50
|
|
|
60
|
-
// Parse command-line arguments for mode flags [ds]
|
|
61
51
|
const args = process.argv.slice(2);
|
|
62
52
|
let modeFlag = '';
|
|
63
53
|
if (args.includes('--light')) modeFlag = ' --light';
|
|
@@ -66,9 +56,7 @@ try {
|
|
|
66
56
|
let commentedAny = false;
|
|
67
57
|
const successfullyCommentedFiles = [];
|
|
68
58
|
|
|
69
|
-
/** Loop through each file to auto-comment [ds] */
|
|
70
59
|
for (const file of filesToComment) {
|
|
71
|
-
// Attempt to read file contents and previous version [ds]
|
|
72
60
|
try {
|
|
73
61
|
const ext = path.extname(file).toLowerCase();
|
|
74
62
|
const contentHead = fs.readFileSync(file, 'utf8');
|
|
@@ -81,7 +69,6 @@ try {
|
|
|
81
69
|
} catch (prevErr) {
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
// Check if file contents have changed (ignoring comments) [ds]
|
|
85
72
|
if (contentPrev) {
|
|
86
73
|
const cleanHead = spliceComments(contentHead, [], 'prune', ext);
|
|
87
74
|
const cleanPrev = spliceComments(contentPrev, [], 'prune', ext);
|
|
@@ -94,16 +81,15 @@ try {
|
|
|
94
81
|
} catch (cleanErr) {
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
// Log and attempt to auto-comment the current file [ds]
|
|
98
84
|
console.log(`[devsplain] Automatically commenting file: ${file}`);
|
|
99
85
|
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
86
|
const cliPath = path.join(__dirname, 'cli.js');
|
|
106
|
-
|
|
87
|
+
const cliArgs = [cliPath, file, '--force'];
|
|
88
|
+
if (modeFlag.trim()) cliArgs.push(modeFlag.trim());
|
|
89
|
+
if (process.env.DS_OVER) cliArgs.push('--overwrite');
|
|
90
|
+
if (process.env.DS_KEEP) cliArgs.push('--keep');
|
|
91
|
+
|
|
92
|
+
execFileSync(process.execPath, cliArgs, { stdio: 'inherit' });
|
|
107
93
|
commentedAny = true;
|
|
108
94
|
successfullyCommentedFiles.push(file);
|
|
109
95
|
} catch (err) {
|
|
@@ -111,14 +97,12 @@ try {
|
|
|
111
97
|
}
|
|
112
98
|
}
|
|
113
99
|
|
|
114
|
-
/** If any files were successfully commented, stage and commit changes [ds] */
|
|
115
100
|
if (commentedAny) {
|
|
116
101
|
for (const file of successfullyCommentedFiles) {
|
|
117
102
|
try {
|
|
118
|
-
|
|
103
|
+
execFileSync('git', ['add', file]);
|
|
119
104
|
} catch (addErr) {}
|
|
120
105
|
}
|
|
121
|
-
|
|
122
106
|
const stagedChanges = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
123
107
|
if (stagedChanges.length > 0) {
|
|
124
108
|
console.log('[devsplain] Staging and committing auto-generated comments...');
|
|
@@ -126,7 +110,6 @@ try {
|
|
|
126
110
|
console.log('[devsplain] Comments committed successfully! Rollback via: git reset --hard HEAD~1');
|
|
127
111
|
}
|
|
128
112
|
}
|
|
129
|
-
// Catch and log any errors that occur during execution [ds]
|
|
130
113
|
} catch (e) {
|
|
131
114
|
console.warn(`[devsplain] Warning: post-commit hook script failed: ${e.message}`);
|
|
132
115
|
}
|
package/bin/setup-hook.js
CHANGED
|
@@ -3,53 +3,79 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** [ds]
|
|
7
|
+
* Automatically detects the local Git repository structure, configures hook directories,
|
|
8
|
+
* allows the user to select a commenting intensity mode, installs executable pre-commit
|
|
9
|
+
* and post-commit shell hooks, and ensures a .devsplainignore file is generated or updated
|
|
10
|
+
* based on existing .gitignore patterns to defer specific paths from automated comment injection.
|
|
11
|
+
*/
|
|
7
12
|
async function installHooks() {
|
|
8
13
|
try {
|
|
14
|
+
// Exec 'git rev-parse --git-dir' to identify the absolute path to the Git directory, normalizing the output by removing any trailing whitespace characters. [ds]
|
|
9
15
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
16
|
+
// Exec 'git rev-parse --show-toplevel' to obtain the absolute path to the top-level directory of the Git repository, parsing away any trailing whitespace to ensure clean path handling. [ds]
|
|
10
17
|
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
18
|
+
// Construct the absolute path to the 'hooks' directory by joining the discovered Git directory path with the subfolder name 'hooks'. [ds]
|
|
11
19
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
20
|
+
// Preemptive check to evaluate if the target 'hooks' directory currently exists in the file system to prevent unnecessary file operations. [ds]
|
|
12
21
|
if (!fs.existsSync(hooksDir)) {
|
|
22
|
+
// Preventively generate the directory hierarchy if it is missing, passing 'recursive: true' so that parent directories are automatically created alongside the target. [ds]
|
|
13
23
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
// Initialize the selected execution mode with a default value of '1', which corresponds to a 'Balanced' commenting strategy, pending further user interaction or console settings. [ds]
|
|
16
27
|
let modeChoice = '1';
|
|
17
|
-
//
|
|
28
|
+
// Check the 'isTTY' property to ascertain whether the current process is running in a true interactive terminal environment capable of capturing standard input. [ds]
|
|
18
29
|
if (process.stdout.isTTY) {
|
|
30
|
+
// Construct an interactive command-line interface, binding the input stream to the standard terminal output to enable typed responses from the executing user. [ds]
|
|
19
31
|
const rl = readline.createInterface({
|
|
20
32
|
input: process.stdin,
|
|
21
33
|
output: process.stdout
|
|
22
34
|
});
|
|
35
|
+
// Define a reusable utility function that wraps the readline interface's question method, encapsulating the asynchronous nature of input polling into a standard resolved Promise. [ds]
|
|
23
36
|
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
24
37
|
|
|
25
|
-
//
|
|
38
|
+
// Print an introductory banner to the standard output, prompting the user to establish the preferred level of conceptual commenting density. [ds]
|
|
26
39
|
console.log('\nSelect default commenting mode for Git commits:');
|
|
40
|
+
// Display the first selectable option, which represents a balanced hybrid of structural JSDoc blocks and contextual inline variable annotations. [ds]
|
|
27
41
|
console.log('1. Balanced (mix of JSDoc and sparse inline comments)');
|
|
42
|
+
// Display the second selectable option, which restricts automated formatting solely to high-level JSDoc block comments sitting directly above declared functions. [ds]
|
|
28
43
|
console.log('2. Light (JSDoc block comments above functions only)');
|
|
44
|
+
// Display the third selectable option, which executes an aggressive generation strategy injecting exhaustive inline step-by-step commentary for every functional line. [ds]
|
|
29
45
|
console.log('3. Full (aggressive inline commenting)');
|
|
30
|
-
|
|
31
|
-
// Loop until a valid mode choice is selected [ds]
|
|
46
|
+
// Establish a continuous, infinite loop necessary to repeatedly prompt the user for terminal input until a fully valid and non-empty alphanumeric selection is secured. [ds]
|
|
32
47
|
while (true) {
|
|
48
|
+
// Suspend execution while awaiting the asynchronous resolution of the user's keyboard input, subsequently clearing any leading or trailing accidental spaces from the selection string. [ds]
|
|
33
49
|
const answer = (await askQuestion('Select (1-3, default: 1): ')).trim();
|
|
50
|
+
// Perform a boolean evaluation to verify if the received input is either an empty string (which invokes the default state) or strictly matches one of the three valid numerical identifiers. [ds]
|
|
34
51
|
if (answer === '' || ['1', '2', '3'].includes(answer)) {
|
|
52
|
+
// Store the validated input into the 'modeChoice' tracking variable; safely defaulting to '1' automatically if the user provided an empty or whitespace-only string selection. [ds]
|
|
35
53
|
modeChoice = answer || '1';
|
|
54
|
+
// Successfully terminate the infinite validation loop upon securing a valid configuration selection, passing execution to the subsequent argument construction logic. [ds]
|
|
36
55
|
break;
|
|
37
56
|
}
|
|
38
57
|
console.log('Invalid choice. Please select 1, 2, or 3.');
|
|
58
|
+
// Output an explicit validation error message to the user's terminal to instruct them that the provided numerical format was out of the defined allowable logical bounds. [ds]
|
|
39
59
|
}
|
|
60
|
+
// Explicitly terminate the active readline interface to cleanly release captured system resources, preventing residual event listeners from blocking the Node.js event loop. [ds]
|
|
40
61
|
rl.close();
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
//
|
|
64
|
+
// Declare a string container, initialized with an empty state, intended to house custom command-line flags corresponding to the identified user selection preferences. [ds]
|
|
44
65
|
let modeArgs = '';
|
|
66
|
+
// Branch execution path if the user dynamically opted into the 'Light' commenting threshold, which dictates minimal architectural styling without deep execution line mapping. [ds]
|
|
45
67
|
if (modeChoice === '2') {
|
|
68
|
+
// Assign the literal command-line flag '--light', prepending a space to facilitate seamless templating into the future shell script argument string. [ds]
|
|
46
69
|
modeArgs = ' --light';
|
|
70
|
+
// Alternate conditional execution path triggered if the user selected the 'Full' commenting threshold, indicating a mandate for high-verbosity and deeply inline code descriptions. [ds]
|
|
47
71
|
} else if (modeChoice === '3') {
|
|
72
|
+
// Assign the literal string value '--full', ensuring a preceding space is present so it consistently formats cleanly as a discrete command-line flag argument. [ds]
|
|
48
73
|
modeArgs = ' --full';
|
|
49
74
|
}
|
|
50
75
|
|
|
51
|
-
//
|
|
76
|
+
// Programmatically assemble the complete absolute file path directing toward the executable segment intended for the Git pre-commit lifecycle event hook. [ds]
|
|
52
77
|
const preCommitHookPath = path.join(hooksDir, 'pre-commit');
|
|
78
|
+
// Generate a shell script body utilizing multi-line template literals to define the structural logic required to programmatically intercept and inspect the pre-commit sequence. [ds]
|
|
53
79
|
const preCommitContent = `#!/bin/sh
|
|
54
80
|
# devsplain native pre-commit hook
|
|
55
81
|
if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
|
|
@@ -57,33 +83,67 @@ if [ -f package.json ] && grep -q '"test"' package.json 2>/dev/null; then
|
|
|
57
83
|
npm test || exit 1
|
|
58
84
|
fi
|
|
59
85
|
`;
|
|
60
|
-
//
|
|
61
|
-
fs.
|
|
86
|
+
// Check the state of the file system to determine whether a pre-existing pre-commit hook artifact has already been deployed to the repository's primary hooking directory. [ds]
|
|
87
|
+
if (fs.existsSync(preCommitHookPath)) {
|
|
88
|
+
// Synchronously read the complete, unmodified contents of the pre-existing hook script and capture it into a string for subsequent signature-based integrity verification. [ds]
|
|
89
|
+
const existing = fs.readFileSync(preCommitHookPath, 'utf8');
|
|
90
|
+
// Perform a string mutation check on the existing file body to ascertain if the deployer's specific native hash signature ('# devsplain native pre-commit hook') is entirely absent. [ds]
|
|
91
|
+
if (!existing.includes('# devsplain native pre-commit hook')) {
|
|
92
|
+
// If the target signature is absent, signaling that an external custom hook is currently in place, safely concatenate and append the required new native testing execution block to the file's tail. [ds]
|
|
93
|
+
fs.appendFileSync(preCommitHookPath, '\n' + preCommitContent);
|
|
94
|
+
} else {
|
|
95
|
+
// If the required native signature is detected within the existing payload, force a full overwrite of the file object to cleanly restore it to the precise native implementation state. [ds]
|
|
96
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Account for the primary cold-start condition where no prior hook file is detected, directly creating the configuration file and persisting the newly templated bash command chain. [ds]
|
|
100
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent);
|
|
101
|
+
}
|
|
102
|
+
// Enclose the specific file permission modification routine within an internal fail-safe error boundary to prevent total deployment aborts upon platform-specific permission barriers. [ds]
|
|
62
103
|
try {
|
|
104
|
+
// Mutate the file system metadata, overriding standard safety permissions to specifically set executable read/write/execute privileges (octal 0o755) upon the newly written artifact. [ds]
|
|
63
105
|
fs.chmodSync(preCommitHookPath, 0o755);
|
|
64
106
|
} catch (err) {}
|
|
65
107
|
|
|
66
|
-
//
|
|
108
|
+
// Evaluate the script's global root directory path, dynamically applying a standard global regular expression substitution to forcibly normalize any Windows-style backslash path breaks into universal forward slashes. [ds]
|
|
67
109
|
const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
|
|
68
110
|
|
|
69
|
-
//
|
|
111
|
+
// Dynamically map the generated environment state to assemble the absolute full path string intended to target and configure the subsequent Git post-commit lifecycle event. [ds]
|
|
70
112
|
const postCommitHookPath = path.join(hooksDir, 'post-commit');
|
|
113
|
+
// Create a formatted string literal using multi-line template interpolation to construct the actual executable shell command sequence invoking localized script handling at the post-commit stage. [ds]
|
|
71
114
|
const postCommitContent = `#!/bin/sh
|
|
72
115
|
# devsplain native post-commit hook
|
|
73
116
|
echo "Auto-generating comments for files in the last commit..."
|
|
74
117
|
node "${postCommitScript}"${modeArgs} || exit 1
|
|
75
118
|
`;
|
|
76
|
-
//
|
|
77
|
-
fs.
|
|
119
|
+
// Execute a fast synchronous existence check against the post-commit path variable to identify if a command chain is already resident and active in the system's Git hook directory. [ds]
|
|
120
|
+
if (fs.existsSync(postCommitHookPath)) {
|
|
121
|
+
// Pull the existing code from the post-commit sequence file stored on the underlying disk format into the active memory context for structural logical inspection and validation. [ds]
|
|
122
|
+
const existing = fs.readFileSync(postCommitHookPath, 'utf8');
|
|
123
|
+
// Test the retrieved content payload against the specific hard-coded signature string of the native pipeline to definitively state whether this artifact was previously managed by this system. [ds]
|
|
124
|
+
if (!existing.includes('# devsplain native post-commit hook')) {
|
|
125
|
+
// If the identification signature is missing, execute a safety-preserve action by appending the new script array to the end of the file, rather than running a destructive overwrite deletion. [ds]
|
|
126
|
+
fs.appendFileSync(postCommitHookPath, '\n' + postCommitContent);
|
|
127
|
+
} else {
|
|
128
|
+
// If the specific native marker is found, the system assumes full ownership and forces an atomic file rewrite to precisely match the expected execution payload and override missing lines. [ds]
|
|
129
|
+
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Address the initial initialization state, bypassing the append logic to directly create the target artifact file and persist the customized templated post-commit execution code. [ds]
|
|
133
|
+
fs.writeFileSync(postCommitHookPath, postCommitContent);
|
|
134
|
+
}
|
|
135
|
+
// Instantiate a nested error containing architecture that protects the vital permission update command from cascading uncaught errors and halting subsequent hook deployment operations. [ds]
|
|
78
136
|
try {
|
|
137
|
+
// Update the file system permissions on the post-commit hook specifically to 0o755 to guarantee that the core operating system is functionally authorized to execute the file as a shell script. [ds]
|
|
79
138
|
fs.chmodSync(postCommitHookPath, 0o755);
|
|
80
139
|
} catch (err) {}
|
|
81
140
|
|
|
82
|
-
//
|
|
141
|
+
// Provide immediate visual stdout feedback to the user's command prompt, proving the successful localization and environment writing of the post-commit file within the local repository. [ds]
|
|
83
142
|
console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
|
|
84
143
|
|
|
85
|
-
//
|
|
144
|
+
// Construct the top-level root path variable by joining the current repository base with the hidden custom generated environment suffix '.devsplainignore'. [ds]
|
|
86
145
|
const ignorePath = path.join(gitRoot, '.devsplainignore');
|
|
146
|
+
// Define a compact hardcoded array payload storing the foundational and universally recognized developer ignore standard defaults, such as local frameworks, build outputs, and IDE-specific state caches. [ds]
|
|
87
147
|
const defaultIgnoreLines = [
|
|
88
148
|
'node_modules/', '.git/', 'dist/', 'build/', 'out/',
|
|
89
149
|
'.next/', '.nuxt/', '.svelte-kit/',
|
|
@@ -91,92 +151,138 @@ node "${postCommitScript}"${modeArgs} || exit 1
|
|
|
91
151
|
'.vscode/', '.idea/', 'coverage/',
|
|
92
152
|
'tests/', '__tests__/', 'fixtures/'
|
|
93
153
|
];
|
|
94
|
-
// List of default patterns to ignore [ds]
|
|
95
154
|
|
|
155
|
+
// Assemble the exact path coordinates in memory for the root-level standard '.gitignore' file, which has underlying authority and precedence in determining the repository's ignored visual outputs. [ds]
|
|
96
156
|
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
157
|
+
// Prepare an empty array structural container for active utilization, designed specifically to intake, parse, and hold the filtered, non-comment active lines from the previously discovered gitignore. [ds]
|
|
97
158
|
let gitignoreLines = [];
|
|
159
|
+
// Verify at the file-system level whether the conventional and standard root-level '.gitignore' has been explicitly manually created and secured within the exact repository target directory. [ds]
|
|
98
160
|
if (fs.existsSync(gitignorePath)) {
|
|
161
|
+
// Load the complete raw textual representation of the discovered '.gitignore' configuration into an in-memory variable utilizing the universally resilient default UTF-8 character encoding system. [ds]
|
|
99
162
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
163
|
+
// Invoke a high-performance string splitting operation using a strictly matching regular expression explicitly designed to handle both standard newline and cross-platform carriage-return formatting styles. [ds]
|
|
100
164
|
gitignoreLines = gitignoreContent.split(/\r?\n/)
|
|
165
|
+
// Pipe the generated string array sequentially to a map operation, systematically iterating and applying strict whitespace-parsing to the exact front and rear boundary of each individual detected line. [ds]
|
|
101
166
|
.map(l => l.trim())
|
|
167
|
+
// Perform a rigorous structural filter over the parsed array, executing a boolean check to actively strip out and eject any empty strings, fully inert lines, and purely informational documentation comment headers. [ds]
|
|
102
168
|
.filter(l => l && !l.startsWith('#'));
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
//
|
|
171
|
+
// Execute a protective condition to verify if the target internal hidden '.devsplainignore' file is completely absent to dictate whether to handle a fresh creation or an existing incremental update. [ds]
|
|
106
172
|
if (!fs.existsSync(ignorePath)) {
|
|
173
|
+
// Compute the exact structural delta by aggressively filtering the raw '.gitignore' payload to physically exclude all internal defaults that the system has already explicitly hardcoded and initialized by design. [ds]
|
|
107
174
|
const gitignoreOnly = gitignoreLines.filter(p => !defaultIgnoreLines.includes(p));
|
|
175
|
+
// Dynamically synthesize the initial sequential payload by collapsing the hardcoded default ignore array values into a single flat string specifically utilizing standard UNIX newline separators. [ds]
|
|
108
176
|
let content = defaultIgnoreLines.join('\n') + '\n';
|
|
177
|
+
// Check if the resulting differential payload contains any logically unmatched and entirely novel patterns generated from the operational '.gitignore' system's custom user environment needs. [ds]
|
|
109
178
|
if (gitignoreOnly.length > 0) {
|
|
179
|
+
// Conditionally chain the newly discovered custom repository rules to the back of the initial payload string, physically interpolating a clear visual banner to indicate external origin. [ds]
|
|
110
180
|
content += '\n# From .gitignore\n' + gitignoreOnly.join('\n') + '\n';
|
|
111
181
|
}
|
|
182
|
+
// Resolve the structural assembly and explicitly write the synthesized logical payload representation down to disk to officially establish the root of the hidden configuration file context. [ds]
|
|
112
183
|
fs.writeFileSync(ignorePath, content);
|
|
184
|
+
// Present an immediate confirmation message directly to the user's terminal prompting, explicitly validating and proving the successful deployment of the new hidden configuration artifact. [ds]
|
|
113
185
|
console.log(`[devsplain] Created .devsplainignore at: ${ignorePath}`);
|
|
186
|
+
// Evaluate if any custom unique patterns survived the comparative filtering process, specifically serving as the logical trigger to broadcast the exact number of migrated items. [ds]
|
|
114
187
|
if (gitignoreOnly.length > 0) {
|
|
188
|
+
// Output a localized high-fidelity statistics string dynamically interpolating the exact numerical length of the moved unique patterns from the external environment into the internal file. [ds]
|
|
115
189
|
console.log(`[devsplain] Merged ${gitignoreOnly.length} pattern(s) from .gitignore into .devsplainignore.`);
|
|
116
190
|
}
|
|
117
191
|
} else {
|
|
192
|
+
// Navigate into the supplementary update pathway specifically designated for handling active repositories which have already previously established and generated their custom hidden ignore file structure. [ds]
|
|
118
193
|
const existingContent = fs.readFileSync(ignorePath, 'utf8');
|
|
194
|
+
// Sync-read the fully active existing payload and programmatically pipeline it through the exact same data parsing and filtering sanitation chain used earlier to extract viable active structural rules. [ds]
|
|
119
195
|
const existingLines = existingContent.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
196
|
+
// Dynamically isolate the exact, fully matching strings belonging to the root outer system level that are entirely and systematically missing from the current active internal existing hidden ignore file. [ds]
|
|
120
197
|
const newPatterns = gitignoreLines.filter(p => !existingLines.includes(p));
|
|
198
|
+
// Perform an evaluation to specifically check if the structural delta extraction operation returned an array populated with even one actively required and missing novel unique operating rule. [ds]
|
|
121
199
|
if (newPatterns.length > 0) {
|
|
200
|
+
// Formulate a targeted append text structure by concatenating the isolated novel lines linearly, automatically prepending a human-readable informational text category banner underneath. [ds]
|
|
122
201
|
const appendContent = '\n# From .gitignore\n' + newPatterns.join('\n') + '\n';
|
|
202
|
+
// Execute a non-destructive, physically conservative file appending command to strictly guarantee that the newly appended array chunk is safely padded down to the final byte of the active text file. [ds]
|
|
123
203
|
fs.appendFileSync(ignorePath, appendContent);
|
|
204
|
+
// Broadcast a specifically targeted command prompt notification, accurately surfacing the exact counted quantity of active novel logical rules that were conditionally propagated into the system local file. [ds]
|
|
124
205
|
console.log(`[devsplain] Merged ${newPatterns.length} new pattern(s) from .gitignore into .devsplainignore.`);
|
|
206
|
+
// Drop into the else-if fallback branch, logically dictated directly by the complete absence of any missing novel patterns, specifically paired with a confirmation that the source external archive contains items. [ds]
|
|
125
207
|
} else if (gitignoreLines.length > 0) {
|
|
208
|
+
// Display a purely confirmatory terminal sequence to log the structural immutability state, explicitly projecting and confirming that the system internal system is already completely complete and synchronized. [ds]
|
|
126
209
|
console.log('[devsplain] .devsplainignore is already up-to-date with .gitignore patterns.');
|
|
127
210
|
}
|
|
128
|
-
// Handle any errors during hook installation [ds]
|
|
129
211
|
}
|
|
130
212
|
|
|
213
|
+
// Enclose the exhaustively complex environment check, write, and setup process inside a robust fail-safe exception container in order to safely capture immediate downstream system interruptions gracefully. [ds]
|
|
131
214
|
} catch (e) {
|
|
215
|
+
// Intercept the caught high-level exception, force a standardized and targeted warning status into the console, specifically flagging the potential unavailability of native Git CLI command systems. [ds]
|
|
132
216
|
console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
|
|
133
217
|
console.warn(e.message);
|
|
134
218
|
}
|
|
135
219
|
}
|
|
136
220
|
|
|
137
|
-
|
|
221
|
+
/** [ds]
|
|
222
|
+
* Scans the active Git repository's hook location, identifies the native developers' hook
|
|
223
|
+
* marker signature, then safely executes a hard unlink process to completely delete
|
|
224
|
+
* the corresponding lifecycle script, alongside confirming the operational status via
|
|
225
|
+
* standard terminal output prompting to the executing user.
|
|
226
|
+
*/
|
|
138
227
|
async function removeHooks() {
|
|
139
228
|
try {
|
|
229
|
+
// Re-invoke the internal Git configuration command to discover the precise filesystem location of the underlying Git state directory utilizing standard cross-platform normalizing output. [ds]
|
|
140
230
|
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
231
|
+
// Synthesize the direct absolute path coordinates required to target the actual, specific folder housing predefined lifecycle event scripts directly operating through the Git process. [ds]
|
|
141
232
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
233
|
+
// Define an immutable object payload map mapping each specific expected lifecycle event identifier strictly to the exact embedded string signature utilized to prove complete system ownership. [ds]
|
|
142
234
|
const hookSignatures = {
|
|
143
235
|
'pre-commit': '# devsplain native pre-commit hook',
|
|
144
236
|
'post-commit': '# devsplain native post-commit hook'
|
|
145
237
|
};
|
|
146
238
|
|
|
239
|
+
// Inception a strict numeric integer tracking variable, structurally initialized to zero, which will exclusively and atomically evaluate and count the total fully successful hard deletions executed. [ds]
|
|
147
240
|
let removed = 0;
|
|
148
|
-
//
|
|
241
|
+
// Initiate a strictly bounded for...of structural traversal cycle directly over the mapped object entries, dynamically isolating the primary key identifier label and the target string marker value. [ds]
|
|
149
242
|
for (const [hookName, signature] of Object.entries(hookSignatures)) {
|
|
243
|
+
// Construct the exact, un-escaped absolute target path string natively resolved by joining the isolated sub-folder base coordinate with the actively evaluated specific lifecycle hook name string. [ds]
|
|
150
244
|
const hookPath = path.join(hooksDir, hookName);
|
|
245
|
+
// Execute a fast, direct filesystem probe to safely verify if the exact isolated file structure actually fully exists and remains physically active on the disk device at the targeted coordinates. [ds]
|
|
151
246
|
if (fs.existsSync(hookPath)) {
|
|
247
|
+
// If affirmative, read the physical file payload from the disk entirely into string memory specifically utilizing the standard UTF-8 encoding protocol to fully prepare for in-memory deep parsing. [ds]
|
|
152
248
|
const content = fs.readFileSync(hookPath, 'utf8');
|
|
249
|
+
// String-search the stored active file memory state to check definitively for the presence of the primary native system hash signature physically marking complete file ownership to this system. [ds]
|
|
153
250
|
if (content.includes(signature)) {
|
|
251
|
+
// Confidently resolve the file pathway and immediately execute a destructive physical unlink operation that entirely and irreversibly removes the target file structure from the local computer's drive. [ds]
|
|
154
252
|
fs.unlinkSync(hookPath);
|
|
253
|
+
// Log an immediate, high-visibility success confirmation to the user prompt area, dynamically interpolating the exact function of the deleted script target alongside its precise absolute storage directory. [ds]
|
|
155
254
|
console.log(`[devsplain] Removed ${hookName} hook at: ${hookPath}`);
|
|
255
|
+
// Safely mutate the previously initialized tracking variable integer by executing a direct increment by one strictly and exclusively upon the completion of undeniably proven targeted file deletion. [ds]
|
|
156
256
|
removed++;
|
|
157
257
|
} else {
|
|
258
|
+
// Otherwise, navigate to a conservative refusal branch indicating that a conflicting external script resides at the exact same directory coordinates, thereby forcing the process to completely skip physical deletion. [ds]
|
|
158
259
|
console.log(`[devsplain] Skipping ${hookName}: not installed by devsplain.`);
|
|
159
260
|
}
|
|
160
261
|
} else {
|
|
262
|
+
// Navigate to a safe fallback branch addressing an empty slot condition where no structural file system paths are discovered to be actively mapped at the target destination point to evaluate. [ds]
|
|
161
263
|
console.log(`[devsplain] No ${hookName} hook found at: ${hookPath}`);
|
|
162
264
|
}
|
|
163
265
|
}
|
|
164
266
|
|
|
165
|
-
//
|
|
267
|
+
// Check if the counter tracking variable was mutated from an initial zero state to prove that at least one highly targeted physical file deletion process actively survived through execution cleanly. [ds]
|
|
166
268
|
if (removed > 0) {
|
|
269
|
+
// Broadcast a localized final success tally update to the user console, dynamically surfacing the exact total count of fully removed lifecycle event operations within the active Git system workspace. [ds]
|
|
167
270
|
console.log(`[devsplain] Successfully removed ${removed} hook(s).`);
|
|
168
271
|
} else {
|
|
272
|
+
// Display an unconditional, globally quiet status report immediately to the user's active standard output, specifically concluding and notifying that absolutely no conflicting active structured installations were found to delete. [ds]
|
|
169
273
|
console.log('[devsplain] No devsplain hooks were found to remove.');
|
|
170
274
|
}
|
|
171
275
|
} catch (e) {
|
|
276
|
+
// Intercept any sudden crashes from the deletion cycle, immediately transforming the raw error payload into a targeted and focused console warning to prevent abrupt termination of the parent CLI execution. [ds]
|
|
172
277
|
console.warn('Warning: Could not remove Git hooks (not inside a git repository or git command missing).');
|
|
173
278
|
console.warn(e.message);
|
|
174
279
|
}
|
|
175
280
|
}
|
|
176
281
|
|
|
177
|
-
//
|
|
282
|
+
// Verify the module's native invocation context by comparing the currently running entry point object literal against the module's own namespace reference to guarantee that this file was the direct core origin trigger. [ds]
|
|
178
283
|
if (require.main === module) {
|
|
284
|
+
// If validation is completely successful, actively trigger and dispatch the asynchronous installation call responsible for deploying all required Git scripts natively within the currently mapped environment context. [ds]
|
|
179
285
|
installHooks();
|
|
180
286
|
}
|
|
181
|
-
//
|
|
287
|
+
// Relocate both primary action utilities into the locally configured module's exported public interface dictionary to explicitly permit perfectly controlled programmatic execution from strictly external source systems. [ds]
|
|
182
288
|
module.exports = { installHooks, removeHooks };
|
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
|
-
|
|
7
|
+
/** Prompts the user for sensitive input with hidden characters and raw stdin handling [ds] */
|
|
8
8
|
function askSecret(query) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
10
|
const rl = readline.createInterface({
|
|
@@ -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 === '\
|
|
26
|
-
|
|
25
|
+
} else if (char === '\u0008' || char === '\u007f') {
|
|
26
|
+
if (key.length > 0) {
|
|
27
|
+
key = key.slice(0, -1);
|
|
28
|
+
process.stdout.write('\b \b');
|
|
29
|
+
}
|
|
30
|
+
} else if (char === '\u0003') {
|
|
31
|
+
process.stdin.setRawMode(false);
|
|
32
|
+
process.exit(130);
|
|
27
33
|
} else {
|
|
28
34
|
key += char;
|
|
29
35
|
process.stdout.write('*');
|
|
@@ -33,9 +39,9 @@ function askSecret(query) {
|
|
|
33
39
|
});
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
/** Migrates legacy single-provider config structure to the new multi-provider 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: {
|
|
@@ -51,7 +57,7 @@ function migrateConfig(oldConfig) {
|
|
|
51
57
|
return oldConfig;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
/** Resolves config from environment variables, file storage, or interactive wizard setup [ds] */
|
|
55
61
|
async function getConfig(forceWizard = false) {
|
|
56
62
|
if (process.env.DEVSPLAIN_API_KEY || process.env.DEVSPLAIN_PROVIDER) {
|
|
57
63
|
const provider = process.env.DEVSPLAIN_PROVIDER || 'gemini';
|
|
@@ -71,11 +77,10 @@ async function getConfig(forceWizard = false) {
|
|
|
71
77
|
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
72
78
|
fileConfig = migrateConfig(JSON.parse(rawData));
|
|
73
79
|
} catch (e) {
|
|
74
|
-
// Ignored, file might be corrupted
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
if (!fileConfig || forceWizard) {
|
|
83
|
+
if (!fileConfig || !fileConfig.activeProvider || !fileConfig.providers || !fileConfig.providers[fileConfig.activeProvider] || forceWizard) {
|
|
79
84
|
let rl = readline.createInterface({
|
|
80
85
|
input: process.stdin,
|
|
81
86
|
output: process.stdout
|
|
@@ -98,7 +103,6 @@ async function getConfig(forceWizard = false) {
|
|
|
98
103
|
console.log(`${i + 1}. ${p}${isActive}`);
|
|
99
104
|
});
|
|
100
105
|
console.log(`\n${savedProviders.length + 1}. Add/Configure a different provider`);
|
|
101
|
-
|
|
102
106
|
const c = await askQuestion(`Select (1-${savedProviders.length + 1}): `);
|
|
103
107
|
const idx = parseInt(c) - 1;
|
|
104
108
|
|
|
@@ -179,7 +183,6 @@ async function getConfig(forceWizard = false) {
|
|
|
179
183
|
provider = providerToConfig;
|
|
180
184
|
const old = config.providers[provider];
|
|
181
185
|
baseUrl = old.baseUrl;
|
|
182
|
-
|
|
183
186
|
let defaultModel = old.model;
|
|
184
187
|
const customModel = await askQuestion(`Model name (press Enter for default '${defaultModel}'): `);
|
|
185
188
|
model = customModel.trim() || defaultModel;
|
|
@@ -248,12 +251,11 @@ async function getConfig(forceWizard = false) {
|
|
|
248
251
|
confirmed = true;
|
|
249
252
|
break;
|
|
250
253
|
} else if (confirm === 'n' || confirm === 'no') {
|
|
251
|
-
break;
|
|
254
|
+
break;
|
|
252
255
|
}
|
|
253
256
|
console.log("Invalid choice. Please enter 'y' or 'n'.");
|
|
254
257
|
}
|
|
255
258
|
} else {
|
|
256
|
-
// User just selected an existing provider and didn't want to update it
|
|
257
259
|
config.activeProvider = providerToConfig;
|
|
258
260
|
console.log(`\nSwitched active provider to ${config.activeProvider}.`);
|
|
259
261
|
confirmed = true;
|
|
@@ -274,9 +276,10 @@ async function getConfig(forceWizard = false) {
|
|
|
274
276
|
...config.providers[config.activeProvider]
|
|
275
277
|
};
|
|
276
278
|
} else {
|
|
279
|
+
const active = fileConfig.providers[fileConfig.activeProvider] || {};
|
|
277
280
|
return {
|
|
278
281
|
provider: fileConfig.activeProvider,
|
|
279
|
-
...
|
|
282
|
+
...active
|
|
280
283
|
};
|
|
281
284
|
}
|
|
282
285
|
}
|
package/lib/llm.js
CHANGED
|
@@ -1,82 +1,151 @@
|
|
|
1
|
-
|
|
1
|
+
/** [ds]
|
|
2
|
+
* Sends an HTTP request with automatic retry logic and timeout management.
|
|
3
|
+
* @param {string} url - The target URL.
|
|
4
|
+
* @param {object} options - Fetch options including method, headers, and body.
|
|
5
|
+
* @param {number} maxRetries - Maximum number of retry attempts.
|
|
6
|
+
* @param {number} initialDelay - Initial delay in milliseconds for exponential backoff.
|
|
7
|
+
* @returns {Promise<Response>} - Returns the successful Response object.
|
|
8
|
+
*/
|
|
2
9
|
async function fetchWithRetry(url, options, maxRetries = 3, initialDelay = 1000) {
|
|
10
|
+
// Initialize a variable to store the last encountered error for potential re-throwing [ds]
|
|
3
11
|
let lastError;
|
|
4
|
-
//
|
|
12
|
+
// Enter a retry loop that executes up to 'maxRetries' times [ds]
|
|
5
13
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
14
|
+
// Create an AbortController to manually trigger request timeouts [ds]
|
|
6
15
|
const controller = new AbortController();
|
|
7
|
-
|
|
16
|
+
// Schedule an abort action after 45 seconds to prevent hanging connections [ds]
|
|
17
|
+
const timeoutId = setTimeout(() => controller.abort(), 45000);
|
|
18
|
+
// Begin a try block to handle potential network or processing exceptions [ds]
|
|
8
19
|
try {
|
|
20
|
+
// Execute the fetch request, passing the URL and options with the abort signal [ds]
|
|
9
21
|
const response = await fetch(url, {
|
|
22
|
+
// Spread existing options into the fetch configuration [ds]
|
|
10
23
|
...options,
|
|
24
|
+
// Attach the abort signal to allow external cancellation [ds]
|
|
11
25
|
signal: controller.signal
|
|
12
26
|
});
|
|
27
|
+
// Clear the timeout timer to clean up the abort signal since the request completed [ds]
|
|
13
28
|
clearTimeout(timeoutId);
|
|
29
|
+
// Validate that a response object was actually received [ds]
|
|
30
|
+
if (!response) {
|
|
31
|
+
// Throw a custom error if the response is missing or null [ds]
|
|
32
|
+
throw new Error("No response received from fetch");
|
|
33
|
+
}
|
|
34
|
+
// Check if the HTTP response indicates success (2xx status) [ds]
|
|
14
35
|
if (response.ok) {
|
|
36
|
+
// Return the response immediately upon successful HTTP completion [ds]
|
|
15
37
|
return response;
|
|
16
38
|
}
|
|
39
|
+
// Identify transient errors: 429 (Too Many Requests) or 5xx (Server Errors) [ds]
|
|
17
40
|
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
|
|
41
|
+
// Store the transient error to trigger a retry in the next loop iteration [ds]
|
|
18
42
|
lastError = new Error(`HTTP Error ${response.status}: ${response.statusText}`);
|
|
19
43
|
} else {
|
|
44
|
+
// For non-transient errors (4xx), return the response directly to let caller handle [ds]
|
|
20
45
|
return response;
|
|
21
46
|
}
|
|
47
|
+
// Catch any synchronous errors thrown during the await or setup [ds]
|
|
22
48
|
} catch (err) {
|
|
49
|
+
// Clear the timeout timer to prevent memory leaks on failure [ds]
|
|
23
50
|
clearTimeout(timeoutId);
|
|
51
|
+
// Check if the error was caused by the manual timeout abort [ds]
|
|
24
52
|
if (err.name === 'AbortError') {
|
|
25
|
-
|
|
53
|
+
// Convert the AbortError into a descriptive timeout message [ds]
|
|
54
|
+
lastError = new Error("Request timed out after 45 seconds");
|
|
26
55
|
} else {
|
|
56
|
+
// Store any other standard errors for potential retry logic [ds]
|
|
27
57
|
lastError = err;
|
|
28
58
|
}
|
|
29
59
|
}
|
|
30
60
|
|
|
61
|
+
// Check if more retry attempts remain before the final try [ds]
|
|
31
62
|
if (attempt < maxRetries - 1) {
|
|
63
|
+
// Calculate exponential backoff delay: base delay multiplied by 2 to the power of attempts [ds]
|
|
32
64
|
const backoffDelay = initialDelay * Math.pow(2, attempt);
|
|
65
|
+
// Log a warning to the console indicating a retry is occurring [ds]
|
|
33
66
|
console.warn(`[devsplain] AI request failed. Retrying in ${backoffDelay}ms... (Attempt ${attempt + 1}/${maxRetries})`);
|
|
67
|
+
// Pause execution for the calculated backoff duration before retrying [ds]
|
|
34
68
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
35
69
|
}
|
|
36
70
|
}
|
|
71
|
+
// Re-throw the last error if all retry attempts have been exhausted [ds]
|
|
37
72
|
throw lastError;
|
|
38
73
|
}
|
|
39
74
|
|
|
40
|
-
|
|
75
|
+
/** [ds]
|
|
76
|
+
* Orchestrate code documentation by calling various AI providers.
|
|
77
|
+
* Handles prompt construction, provider-specific API calls, and response parsing.
|
|
78
|
+
* @param {string} code - The source code to analyze.
|
|
79
|
+
* @param {string} language - The programming language and file extension.
|
|
80
|
+
* @param {object} config - Provider configuration details.
|
|
81
|
+
* @param {string} mode - Documentation density mode ('default', 'light', 'full', 'clean').
|
|
82
|
+
* @returns {Promise<Array>} - Array of comment insertion objects.
|
|
83
|
+
*/
|
|
41
84
|
async function getComments(code, language, config, mode = 'default') {
|
|
85
|
+
// Split source code into an array of lines, handling Windows and Unix newline styles [ds]
|
|
42
86
|
const lines = code.split(/\r?\n/);
|
|
87
|
+
// Prepend line numbers to each line to help the AI map comments to specific locations [ds]
|
|
43
88
|
const numberedCode = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
|
|
44
89
|
|
|
45
|
-
// Extract the file extension
|
|
90
|
+
// Extract the file extension using a regular expression [ds]
|
|
46
91
|
const extMatch = language.match(/\.[0-9a-z]+$/i);
|
|
92
|
+
// Normalize the extension to lowercase or default to empty string if missing [ds]
|
|
47
93
|
const ext = extMatch ? extMatch[0].toLowerCase() : '';
|
|
94
|
+
// Determine if the language is Python [ds]
|
|
48
95
|
const isPython = ext === '.py';
|
|
49
|
-
|
|
96
|
+
// Determine if the language is Ruby or Shell Script [ds]
|
|
97
|
+
const isRubyOrShell = ['.rb', '.sh'].includes(ext);
|
|
98
|
+
// Determine if the language is HTML-based (HTML, Vue, Svelte) [ds]
|
|
50
99
|
const isHTML = ['.html', '.vue', '.svelte'].includes(ext);
|
|
100
|
+
// Determine if the language is CSS or SCSS [ds]
|
|
101
|
+
const isCss = ['.css', '.scss'].includes(ext);
|
|
102
|
+
// Determine if the language is SQL [ds]
|
|
51
103
|
const isSql = ext === '.sql';
|
|
52
|
-
|
|
53
|
-
// Define the single-line comment token and examples [ds]
|
|
104
|
+
// Initialize comment syntax tokens for standard C-like languages [ds]
|
|
54
105
|
let singleLineToken = '//';
|
|
55
106
|
let blockExample = '/** Calculates the total price */';
|
|
56
107
|
let inlineExample = '// Check for null values';
|
|
57
108
|
|
|
109
|
+
// Adjust comment syntax for Python and Shell-based languages [ds]
|
|
58
110
|
if (isPython || isRubyOrShell) {
|
|
59
111
|
singleLineToken = '#';
|
|
60
112
|
blockExample = '# Calculates the total price';
|
|
61
113
|
inlineExample = '# Check for null values';
|
|
114
|
+
// Adjust comment syntax for HTML template languages [ds]
|
|
62
115
|
} else if (isHTML) {
|
|
63
116
|
singleLineToken = '<!--';
|
|
64
117
|
blockExample = '<!-- Calculates the total price -->';
|
|
65
118
|
inlineExample = '<!-- Check for null values -->';
|
|
119
|
+
// Adjust comment syntax for CSS stylesheets [ds]
|
|
120
|
+
} else if (isCss) {
|
|
121
|
+
singleLineToken = '/*';
|
|
122
|
+
blockExample = '/* Calculates the total price */';
|
|
123
|
+
inlineExample = '/* Check for null values */';
|
|
124
|
+
// Adjust comment syntax for SQL queries [ds]
|
|
66
125
|
} else if (isSql) {
|
|
67
126
|
singleLineToken = '--';
|
|
68
127
|
blockExample = '-- Calculates the total price';
|
|
69
128
|
inlineExample = '-- Check for null values';
|
|
70
129
|
}
|
|
71
130
|
|
|
72
|
-
//
|
|
131
|
+
// Set default prompting instruction for standard documentation [ds]
|
|
73
132
|
let instruction = `Provide block comments above functions and sparse inline comments for complex logic.`;
|
|
133
|
+
// Modify instruction if minimal documentation is requested [ds]
|
|
74
134
|
if (mode === 'light') {
|
|
75
135
|
instruction = `Provide ONLY block comments above functions. Keep it minimal.`;
|
|
136
|
+
// Modify instruction for exhaustive step-by-step documentation mode [ds]
|
|
76
137
|
} else if (mode === 'full') {
|
|
77
138
|
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
139
|
}
|
|
79
140
|
|
|
141
|
+
// Define a placeholder rule for comment syntax restrictions [ds]
|
|
142
|
+
let rule5 = `5. IMPORTANT: Use ONLY ${singleLineToken} for comments. DO NOT use docstrings or multi-line string literals like """ or ''' for comments.`;
|
|
143
|
+
// Override the syntax rule specifically for CSS files [ds]
|
|
144
|
+
if (isCss) {
|
|
145
|
+
rule5 = `5. IMPORTANT: In CSS/SCSS, you MUST use /* ... */ for comments. DO NOT use // comments under any circumstances.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Construct the full prompt combining rules, examples, and the numbered source code [ds]
|
|
80
149
|
let prompt = `
|
|
81
150
|
You are a code documentation engine. Analyze the following ${language} code which has line numbers prepended to it.
|
|
82
151
|
${instruction}
|
|
@@ -86,7 +155,7 @@ CRITICAL RULES:
|
|
|
86
155
|
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
156
|
3. Do NOT include the original code in your response.
|
|
88
157
|
4. If no comments are needed, return an empty array: [].
|
|
89
|
-
|
|
158
|
+
${rule5}
|
|
90
159
|
|
|
91
160
|
Example Output:
|
|
92
161
|
[
|
|
@@ -98,13 +167,18 @@ Here is the source code:
|
|
|
98
167
|
${numberedCode}
|
|
99
168
|
`.trim();
|
|
100
169
|
|
|
170
|
+
// Initialize a variable to hold the raw text output from the AI provider [ds]
|
|
101
171
|
let textResponse = "";
|
|
102
172
|
|
|
103
|
-
//
|
|
173
|
+
// Branch to handle the Google Gemini API provider [ds]
|
|
104
174
|
if (config.provider === 'gemini') {
|
|
175
|
+
// Construct the Google Gemini API endpoint URL with model and API key [ds]
|
|
105
176
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
|
|
177
|
+
// Declare a variable to store the parsed API response [ds]
|
|
106
178
|
let data;
|
|
179
|
+
// Begin error handling for the Gemini API call [ds]
|
|
107
180
|
try {
|
|
181
|
+
// Send the POST request to the Gemini API with the constructed prompt [ds]
|
|
108
182
|
const response = await fetchWithRetry(url, {
|
|
109
183
|
method: 'POST',
|
|
110
184
|
headers: {
|
|
@@ -114,19 +188,37 @@ ${numberedCode}
|
|
|
114
188
|
"contents": [{ "parts": [{ "text": prompt }] }]
|
|
115
189
|
})
|
|
116
190
|
});
|
|
191
|
+
// Parse the JSON response body from the Gemini service [ds]
|
|
117
192
|
data = await response.json();
|
|
193
|
+
// Wrap network or fetch errors in a standard descriptive error [ds]
|
|
118
194
|
} catch (error) {
|
|
119
|
-
throw new Error(
|
|
195
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
120
196
|
}
|
|
197
|
+
// Check if the Gemini response contains an error object [ds]
|
|
121
198
|
if (data.error) {
|
|
122
|
-
|
|
199
|
+
// Extract the human-readable error message from the Google-specific structure [ds]
|
|
200
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
201
|
+
// Throw a normalized error based on the API error details [ds]
|
|
202
|
+
throw new Error(`API Error: ${msg}`);
|
|
203
|
+
}
|
|
204
|
+
// Validate that the Gemini response structure contains the expected content [ds]
|
|
205
|
+
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts[0]) {
|
|
206
|
+
// Capture the finish reason to provide context for empty content errors [ds]
|
|
207
|
+
const reason = data.candidates?.[0]?.finishReason || 'Unknown error';
|
|
208
|
+
// Throw an error indicating the AI returned no usable content [ds]
|
|
209
|
+
throw new Error(`AI Provider returned no content (finish reason: ${reason})`);
|
|
123
210
|
}
|
|
211
|
+
// Extract the raw text content from the Gemini response candidates [ds]
|
|
124
212
|
textResponse = data.candidates[0].content.parts[0].text;
|
|
213
|
+
// Branch to handle the Anthropic Claude API provider [ds]
|
|
125
214
|
} else if (config.provider === 'claude') {
|
|
126
|
-
//
|
|
215
|
+
// Construct the Anthropic API endpoint URL [ds]
|
|
127
216
|
const url = `${config.baseUrl}/v1/messages`;
|
|
217
|
+
// Declare a variable to store the parsed Anthropic API response [ds]
|
|
128
218
|
let data;
|
|
219
|
+
// Begin error handling for the Anthropic API call [ds]
|
|
129
220
|
try {
|
|
221
|
+
// Send the POST request to Anthropic with version headers and API key [ds]
|
|
130
222
|
const response = await fetchWithRetry(url, {
|
|
131
223
|
method: 'POST',
|
|
132
224
|
headers: {
|
|
@@ -143,97 +235,198 @@ ${numberedCode}
|
|
|
143
235
|
}]
|
|
144
236
|
})
|
|
145
237
|
});
|
|
238
|
+
// Parse the JSON response body from the Anthropic service [ds]
|
|
146
239
|
data = await response.json();
|
|
240
|
+
// Wrap network or fetch errors in a standard descriptive error [ds]
|
|
147
241
|
} catch (error) {
|
|
148
|
-
throw new Error(
|
|
242
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
149
243
|
}
|
|
244
|
+
// Check if the Anthropic response contains an error object [ds]
|
|
150
245
|
if (data.error) {
|
|
151
|
-
|
|
246
|
+
// Extract the human-readable error message from the Anthropic response [ds]
|
|
247
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
248
|
+
// Throw a normalized error based on the API error details [ds]
|
|
249
|
+
throw new Error(`API Error: ${msg}`);
|
|
152
250
|
}
|
|
251
|
+
// Validate that the Anthropic response structure contains valid text content [ds]
|
|
252
|
+
if (!data.content || !data.content[0] || typeof data.content[0].text !== 'string') {
|
|
253
|
+
// Throw an error with the raw JSON if the structure is unexpected [ds]
|
|
254
|
+
throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
|
|
255
|
+
}
|
|
256
|
+
// Extract the text content from the first message block [ds]
|
|
153
257
|
textResponse = data.content[0].text;
|
|
154
258
|
}
|
|
155
|
-
//
|
|
259
|
+
// Default branch for OpenAI-compatible APIs (OpenAI, Groq, etc.) [ds]
|
|
156
260
|
else {
|
|
261
|
+
// Construct the OpenAI-compatible chat completions endpoint [ds]
|
|
157
262
|
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
263
|
+
// Declare a variable to store the parsed OpenAI-compatible response [ds]
|
|
158
264
|
let data;
|
|
159
265
|
|
|
266
|
+
// Initialize the request body structure with model and message content [ds]
|
|
267
|
+
const reqBody = {
|
|
268
|
+
"model": config.model,
|
|
269
|
+
"messages": [{
|
|
270
|
+
"role": "user",
|
|
271
|
+
"content": prompt
|
|
272
|
+
}]
|
|
273
|
+
};
|
|
274
|
+
// Apply specific token limits for Groq provider to avoid issues [ds]
|
|
275
|
+
if (config.provider === 'groq') {
|
|
276
|
+
reqBody.max_tokens = 1000;
|
|
277
|
+
// Apply default high token limit for other OpenAI-compatible providers [ds]
|
|
278
|
+
} else {
|
|
279
|
+
reqBody.max_tokens = 8192;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Begin error handling for the OpenAI-compatible API call [ds]
|
|
160
283
|
try {
|
|
284
|
+
// Send the POST request with Bearer token authentication [ds]
|
|
161
285
|
const response = await fetchWithRetry(url, {
|
|
162
286
|
method: 'POST',
|
|
163
287
|
headers: {
|
|
164
288
|
'Content-Type': 'application/json',
|
|
165
289
|
'Authorization': `Bearer ${config.apiKey}`
|
|
166
290
|
},
|
|
167
|
-
body: JSON.stringify(
|
|
168
|
-
"model": config.model,
|
|
169
|
-
"messages": [{
|
|
170
|
-
"role": "user",
|
|
171
|
-
"content": prompt
|
|
172
|
-
}]
|
|
173
|
-
})
|
|
291
|
+
body: JSON.stringify(reqBody)
|
|
174
292
|
});
|
|
293
|
+
// Parse the JSON response body from the OpenAI-compatible service [ds]
|
|
175
294
|
data = await response.json();
|
|
295
|
+
// Wrap network or fetch errors in a standard descriptive error [ds]
|
|
176
296
|
} catch (error) {
|
|
177
|
-
throw new Error(
|
|
297
|
+
throw new Error(`AI Provider Request Failed: ${error.message}`);
|
|
178
298
|
}
|
|
299
|
+
// Check if the OpenAI-compatible response contains an error object [ds]
|
|
179
300
|
if (data.error) {
|
|
180
|
-
|
|
301
|
+
// Extract the human-readable error message from the OpenAI error structure [ds]
|
|
302
|
+
const msg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
|
|
303
|
+
// Throw a normalized error based on the API error details [ds]
|
|
304
|
+
throw new Error(`API Error: ${msg}`);
|
|
305
|
+
}
|
|
306
|
+
// Validate that the OpenAI response structure contains a valid message object [ds]
|
|
307
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || typeof data.choices[0].message.content !== 'string') {
|
|
308
|
+
// Throw an error with the raw JSON if the structure is unexpected [ds]
|
|
309
|
+
throw new Error(`AI Provider returned an unexpected response structure: ${JSON.stringify(data)}`);
|
|
181
310
|
}
|
|
311
|
+
// Extract the text content from the first choice's message [ds]
|
|
182
312
|
textResponse = data.choices[0].message.content;
|
|
183
313
|
}
|
|
184
314
|
|
|
315
|
+
// Remove leading and trailing whitespace from the AI text output [ds]
|
|
185
316
|
let cleanText = textResponse.trim();
|
|
317
|
+
// Locate the starting index of the JSON array [ds]
|
|
186
318
|
const start = cleanText.indexOf('[');
|
|
319
|
+
// Locate the ending index of the JSON array [ds]
|
|
187
320
|
const end = cleanText.lastIndexOf(']');
|
|
188
|
-
|
|
189
|
-
|
|
321
|
+
// Check if a JSON array was actually found in the text [ds]
|
|
322
|
+
if (start !== -1) {
|
|
323
|
+
// Validate that the end index exists and comes after the start index [ds]
|
|
324
|
+
if (end !== -1 && end >= start) {
|
|
325
|
+
// Slice the text to contain only the JSON array portion [ds]
|
|
326
|
+
cleanText = cleanText.substring(start, end + 1);
|
|
327
|
+
} else {
|
|
328
|
+
// If ']' is missing, look for the last closing brace to fix truncated JSON [ds]
|
|
329
|
+
const lastBrace = cleanText.lastIndexOf('}');
|
|
330
|
+
// Ensure the found brace is actually within the current text segment [ds]
|
|
331
|
+
if (lastBrace > start) {
|
|
332
|
+
// Manually append the missing closing bracket to make valid JSON [ds]
|
|
333
|
+
cleanText = cleanText.substring(start, lastBrace + 1) + ']';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
190
336
|
}
|
|
191
337
|
|
|
338
|
+
// Declare a variable to store the parsed JSON object [ds]
|
|
192
339
|
let parsed;
|
|
340
|
+
// Attempt to parse the cleaned text string into a JavaScript object [ds]
|
|
193
341
|
try {
|
|
194
|
-
//
|
|
342
|
+
// Execute the JSON parsing operation [ds]
|
|
195
343
|
parsed = JSON.parse(cleanText);
|
|
344
|
+
// Catch parsing errors if the text is malformed [ds]
|
|
196
345
|
} catch (e) {
|
|
346
|
+
// Throw a descriptive error including the raw response for debugging [ds]
|
|
197
347
|
throw new Error(`Parsing Error: Failed to parse LLM response as JSON. Raw response was:\n${textResponse}`);
|
|
198
348
|
}
|
|
199
349
|
|
|
350
|
+
// Validate that the parsed result is a JavaScript array [ds]
|
|
200
351
|
if (!Array.isArray(parsed)) {
|
|
352
|
+
// Throw an error if the top-level JSON structure is not an array [ds]
|
|
201
353
|
throw new Error("Schema Error: LLM response is not a JSON array.");
|
|
202
354
|
}
|
|
203
355
|
|
|
204
|
-
//
|
|
356
|
+
// Iterate over each comment object in the parsed array for validation [ds]
|
|
205
357
|
for (const item of parsed) {
|
|
358
|
+
// Verify that each item is a real object and not null or a primitive [ds]
|
|
206
359
|
if (typeof item !== 'object' || item === null) {
|
|
360
|
+
// Throw a schema error for invalid object types [ds]
|
|
207
361
|
throw new Error("Schema Error: Array elements must be objects.");
|
|
208
362
|
}
|
|
363
|
+
// Validate that the 'line' property is a positive integer [ds]
|
|
209
364
|
if (!Number.isInteger(item.line) || item.line <= 0) {
|
|
365
|
+
// Throw a schema error for invalid line numbers [ds]
|
|
210
366
|
throw new Error("Schema Error: 'line' must be a positive integer.");
|
|
211
367
|
}
|
|
212
368
|
|
|
369
|
+
// Enforce specific schema rules for data cleaning mode [ds]
|
|
213
370
|
if (mode === 'clean') {
|
|
371
|
+
// Ensure the 'action' property is 'delete' if in clean mode [ds]
|
|
214
372
|
if (item.action !== 'delete') {
|
|
373
|
+
// Throw a schema error for invalid actions in clean mode [ds]
|
|
215
374
|
throw new Error("Schema Error: 'action' must be 'delete' in clean mode.");
|
|
216
375
|
}
|
|
376
|
+
// Handle standard documentation modes that require comment text [ds]
|
|
217
377
|
} else {
|
|
378
|
+
// Verify that the 'comment' property exists and is a string [ds]
|
|
218
379
|
if (typeof item.comment !== 'string') {
|
|
380
|
+
// Throw a schema error if comment text is missing or not a string [ds]
|
|
219
381
|
throw new Error("Schema Error: 'comment' must be a string.");
|
|
220
382
|
}
|
|
221
383
|
|
|
384
|
+
// Normalize the comment text by trimming whitespace [ds]
|
|
222
385
|
const trimmedComment = item.comment.trim();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
386
|
+
// Split the comment into lines to validate syntax per line [ds]
|
|
387
|
+
const commentLines = trimmedComment.split(/\r?\n/);
|
|
388
|
+
// Track whether we are currently inside a block comment [ds]
|
|
389
|
+
let inBlock = false;
|
|
390
|
+
// Loop through each line of the comment string [ds]
|
|
391
|
+
for (const cl of commentLines) {
|
|
392
|
+
// Trim leading/trailing whitespace from the current comment line [ds]
|
|
393
|
+
const tcl = cl.trim();
|
|
394
|
+
// Skip empty lines during validation [ds]
|
|
395
|
+
if (!tcl) continue;
|
|
396
|
+
// Check if this line is part of the continuation of a block comment [ds]
|
|
397
|
+
if (inBlock) {
|
|
398
|
+
// Detect if the line closes a block comment (CSS/HTML style) [ds]
|
|
399
|
+
if (tcl.includes('*/') || tcl.includes('-->')) {
|
|
400
|
+
// Mark block comment state as false if a closing marker is found [ds]
|
|
401
|
+
inBlock = false;
|
|
402
|
+
}
|
|
403
|
+
// Skip validation for lines inside a block comment [ds]
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Check if the line starts with a valid comment marker [ds]
|
|
407
|
+
const startsWithMarker =
|
|
408
|
+
tcl.startsWith('//') ||
|
|
409
|
+
tcl.startsWith('/*') ||
|
|
410
|
+
tcl.startsWith('*') ||
|
|
411
|
+
tcl.startsWith('#') ||
|
|
412
|
+
tcl.startsWith('<!--') ||
|
|
413
|
+
tcl.startsWith('--');
|
|
414
|
+
// Throw a strict security error if a line lacks a comment prefix [ds]
|
|
415
|
+
if (!startsWithMarker) {
|
|
416
|
+
throw new Error(`Security Error: Comment on line ${item.line} contains invalid non-comment line: "${tcl}"`);
|
|
417
|
+
}
|
|
418
|
+
// Detect if a line opens a block comment without closing it [ds]
|
|
419
|
+
if ((tcl.startsWith('/*') && !tcl.includes('*/')) || (tcl.startsWith('<!--') && !tcl.includes('-->'))) {
|
|
420
|
+
// Set the block comment state to true for subsequent line processing [ds]
|
|
421
|
+
inBlock = true;
|
|
422
|
+
}
|
|
232
423
|
}
|
|
233
424
|
}
|
|
234
425
|
}
|
|
235
426
|
|
|
427
|
+
// Return the fully validated and parsed array of comments [ds]
|
|
236
428
|
return parsed;
|
|
237
429
|
}
|
|
238
430
|
|
|
431
|
+
// Export the main function for use by external modules [ds]
|
|
239
432
|
module.exports = { getComments };
|
package/package.json
CHANGED