devsplain 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { getComments } = require('../lib/llm.js');
3
+ const { getComments, runWithConcurrency, resetConcurrency } = require('../lib/llm.js');
4
4
  const { getConfig } = require('../lib/config.js');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
@@ -817,7 +817,7 @@ Options:
817
817
  };
818
818
 
819
819
  let filepath = '.';
820
- const flagKeys = ['--provider', '--model', '--api-key', '--base-url'];
820
+ const flagKeys = ['--provider', '--model', '--api-key', '--base-url', '--concurrency'];
821
821
  for (let i = 0; i < args.length; i++) {
822
822
  const arg = args[i];
823
823
  if (arg.startsWith('--')) {
@@ -877,6 +877,10 @@ Options:
877
877
 
878
878
  const isOverwrite = (hasOverwriteFlag || config.autoPrune) && !hasKeepFlag;
879
879
 
880
+ // Parse --concurrency flag (default: 2, max: 5, min: 1) [ds]
881
+ const cliConcurrency = parseInt(getArgValue('--concurrency'), 10);
882
+ const concurrencyLevel = (cliConcurrency && cliConcurrency >= 1 && cliConcurrency <= 5) ? cliConcurrency : 2;
883
+
880
884
  let userIgnorePatterns = [];
881
885
  try {
882
886
  const ignorePath = path.join(process.cwd(), '.devsplainignore');
@@ -908,80 +912,95 @@ Options:
908
912
  return false;
909
913
  }
910
914
 
911
- async function processPath(targetPath) {
915
+ const validExtensions = [
916
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
917
+ '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
918
+ '.swift', '.kt', '.dart', '.sh', '.sql'
919
+ ];
920
+
921
+ // Separate file discovery from processing for concurrency support [ds]
922
+ function collectFiles(targetPath) {
923
+ const collected = [];
912
924
  const stats = fs.statSync(targetPath);
913
925
 
914
- if (isPathIgnored(targetPath)) {
915
- return;
916
- }
926
+ if (isPathIgnored(targetPath)) return collected;
917
927
 
918
928
  if (stats.isDirectory()) {
919
929
  console.log(`\n Scanning directory: ${targetPath}`);
920
930
  const items = fs.readdirSync(targetPath);
921
931
  for (const item of items) {
922
- const fullPath = path.join(targetPath, item);
923
- await processPath(fullPath);
932
+ collected.push(...collectFiles(path.join(targetPath, item)));
924
933
  }
925
- }
926
- else if (stats.isFile()) {
934
+ } else if (stats.isFile()) {
927
935
  const ext = path.extname(targetPath).toLowerCase();
928
- const validExtensions = [
929
- '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.vue', '.svelte',
930
- '.py', '.java', '.c', '.cpp', '.cs', '.go', '.rb', '.php', '.rs',
931
- '.swift', '.kt', '.dart', '.sh', '.sql'
932
- ];
933
-
934
- if (!validExtensions.includes(ext)) {
935
- return;
936
- }
936
+ if (!validExtensions.includes(ext)) return collected;
937
937
 
938
- const filename = path.basename(targetPath);
939
938
  const data = fs.readFileSync(targetPath, 'utf-8');
940
939
  if (data.trim() === '') {
941
- console.log(` Skipping ${filename} (Empty File)`);
942
- return;
940
+ console.log(` Skipping ${path.basename(targetPath)} (Empty File)`);
941
+ return collected;
943
942
  }
943
+ collected.push(targetPath);
944
+ }
945
+ return collected;
946
+ }
944
947
 
945
- console.log(` Analyzing ${filename} in ${mode} mode...`);
946
- try {
947
- let comments = [];
948
- let commentedCode;
949
- if (mode !== 'clean' && mode !== 'prune') {
950
- const preProcessMode = isOverwrite ? 'prune' : 'clean';
951
- const cleanData = spliceComments(data, [], preProcessMode, ext);
952
- comments = await getComments(cleanData, filename, config, mode);
953
- commentedCode = spliceComments(cleanData, comments, mode, ext);
954
- } else {
955
- commentedCode = spliceComments(data, [], mode, ext);
956
- }
957
- if (isDryRun) {
958
- console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
959
- console.log(commentedCode);
960
- console.log(`---------------------------------------\n`);
961
- const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
962
- if (answer.toLowerCase() === 'write') {
963
- const tempPath = targetPath + '.tmp';
964
- fs.writeFileSync(tempPath, commentedCode, 'utf8');
965
- fs.renameSync(tempPath, targetPath);
966
- console.log(` Successfully saved ${targetPath}`);
967
- } else {
968
- console.log(` Skipped ${targetPath}`);
969
- }
970
- } else {
948
+ async function processSingleFile(targetPath) {
949
+ const filename = path.basename(targetPath);
950
+ const ext = path.extname(targetPath).toLowerCase();
951
+ const data = fs.readFileSync(targetPath, 'utf-8');
952
+
953
+ console.log(` Analyzing ${filename} in ${mode} mode...`);
954
+ try {
955
+ let comments = [];
956
+ let commentedCode;
957
+ if (mode !== 'clean' && mode !== 'prune') {
958
+ const preProcessMode = isOverwrite ? 'prune' : 'clean';
959
+ const cleanData = spliceComments(data, [], preProcessMode, ext);
960
+ comments = await getComments(cleanData, filename, config, mode);
961
+ commentedCode = spliceComments(cleanData, comments, mode, ext);
962
+ } else {
963
+ commentedCode = spliceComments(data, [], mode, ext);
964
+ }
965
+ if (isDryRun) {
966
+ console.log(`\n --- DRY RUN PREVIEW: ${filename} ---`);
967
+ console.log(commentedCode);
968
+ console.log(`---------------------------------------\n`);
969
+ const answer = await askQuestion("Type 'write' to save to file, or press any key to discard: ");
970
+ if (answer.toLowerCase() === 'write') {
971
971
  const tempPath = targetPath + '.tmp';
972
972
  fs.writeFileSync(tempPath, commentedCode, 'utf8');
973
973
  fs.renameSync(tempPath, targetPath);
974
- console.log(` Successfully commented ${targetPath}`);
974
+ console.log(` Successfully saved ${targetPath}`);
975
+ } else {
976
+ console.log(` Skipped ${targetPath}`);
975
977
  }
976
- successCount++;
977
- } catch (err) {
978
- console.error(` Error processing ${filename}: ${err.message}`);
979
- failCount++;
978
+ } else {
979
+ const tempPath = targetPath + '.tmp';
980
+ fs.writeFileSync(tempPath, commentedCode, 'utf8');
981
+ fs.renameSync(tempPath, targetPath);
982
+ console.log(` Successfully commented ${targetPath}`);
980
983
  }
984
+ successCount++;
985
+ } catch (err) {
986
+ console.error(` Error processing ${filename}: ${err.message}`);
987
+ failCount++;
981
988
  }
982
989
  }
983
990
 
984
- await processPath(filepath);
991
+ // Collect all eligible files, then process with adaptive concurrency [ds]
992
+ const filesToProcess = collectFiles(filepath);
993
+
994
+ // Dry-run mode processes files serially to allow interactive prompts [ds]
995
+ if (isDryRun || mode === 'clean' || mode === 'prune') {
996
+ for (const file of filesToProcess) {
997
+ await processSingleFile(file);
998
+ }
999
+ } else {
1000
+ resetConcurrency(concurrencyLevel);
1001
+ console.log(`\n Processing ${filesToProcess.length} file(s) with concurrency: ${concurrencyLevel}`);
1002
+ await runWithConcurrency(filesToProcess, processSingleFile);
1003
+ }
985
1004
 
986
1005
  if (failCount > 0 && successCount === 0) {
987
1006
  console.error("\nFailed: No files were successfully commented.");
@@ -3,47 +3,39 @@ 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
- // Get a list of changed files in the last commit [ds]
33
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
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,7 +81,6 @@ 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
86
  const cliPath = path.join(__dirname, 'cli.js');
@@ -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
- // Installs Git hooks for automatic comment generation [ds]
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
- // Prompt user for commenting mode if running in a TTY [ds]
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
- // Display commenting mode options to the user [ds]
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
- // Determine mode arguments based on user selection [ds]
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
- // Create pre-commit hook script [ds]
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,51 +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
- // Write pre-commit hook safely [ds]
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]
61
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]
62
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]
63
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]
64
93
  fs.appendFileSync(preCommitHookPath, '\n' + preCommitContent);
65
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]
66
96
  fs.writeFileSync(preCommitHookPath, preCommitContent);
67
97
  }
68
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]
69
100
  fs.writeFileSync(preCommitHookPath, preCommitContent);
70
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]
71
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]
72
105
  fs.chmodSync(preCommitHookPath, 0o755);
73
106
  } catch (err) {}
74
107
 
75
- // Path to post-commit script [ds]
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]
76
109
  const postCommitScript = path.join(__dirname, 'post-commit.js').replace(/\\/g, '/');
77
110
 
78
- // Create post-commit hook script [ds]
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]
79
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]
80
114
  const postCommitContent = `#!/bin/sh
81
115
  # devsplain native post-commit hook
82
116
  echo "Auto-generating comments for files in the last commit..."
83
117
  node "${postCommitScript}"${modeArgs} || exit 1
84
118
  `;
85
- // Write post-commit hook safely [ds]
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]
86
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]
87
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]
88
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]
89
126
  fs.appendFileSync(postCommitHookPath, '\n' + postCommitContent);
90
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]
91
129
  fs.writeFileSync(postCommitHookPath, postCommitContent);
92
130
  }
93
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]
94
133
  fs.writeFileSync(postCommitHookPath, postCommitContent);
95
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]
96
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]
97
138
  fs.chmodSync(postCommitHookPath, 0o755);
98
139
  } catch (err) {}
99
140
 
100
- // Log successful installation of post-commit hook [ds]
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]
101
142
  console.log(`[devsplain] Git post-commit hook successfully installed at: ${postCommitHookPath}`);
102
143
 
103
- // Path to devsplain ignore file [ds]
144
+ // Construct the top-level root path variable by joining the current repository base with the hidden custom generated environment suffix '.devsplainignore'. [ds]
104
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]
105
147
  const defaultIgnoreLines = [
106
148
  'node_modules/', '.git/', 'dist/', 'build/', 'out/',
107
149
  '.next/', '.nuxt/', '.svelte-kit/',
@@ -109,92 +151,138 @@ node "${postCommitScript}"${modeArgs} || exit 1
109
151
  '.vscode/', '.idea/', 'coverage/',
110
152
  'tests/', '__tests__/', 'fixtures/'
111
153
  ];
112
- // List of default patterns to ignore [ds]
113
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]
114
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]
115
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]
116
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]
117
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]
118
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]
119
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]
120
168
  .filter(l => l && !l.startsWith('#'));
121
169
  }
122
170
 
123
- // Check if .gitignore file exists [ds]
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]
124
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]
125
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]
126
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]
127
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]
128
180
  content += '\n# From .gitignore\n' + gitignoreOnly.join('\n') + '\n';
129
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]
130
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]
131
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]
132
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]
133
189
  console.log(`[devsplain] Merged ${gitignoreOnly.length} pattern(s) from .gitignore into .devsplainignore.`);
134
190
  }
135
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]
136
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]
137
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]
138
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]
139
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]
140
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]
141
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]
142
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]
143
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]
144
209
  console.log('[devsplain] .devsplainignore is already up-to-date with .gitignore patterns.');
145
210
  }
146
- // Handle any errors during hook installation [ds]
147
211
  }
148
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]
149
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]
150
216
  console.warn('Warning: Could not set up Git hooks (not inside a git repository or git command missing).');
151
217
  console.warn(e.message);
152
218
  }
153
219
  }
154
220
 
155
- // Removes Git hooks installed by devsplain [ds]
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
+ */
156
227
  async function removeHooks() {
157
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]
158
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]
159
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]
160
234
  const hookSignatures = {
161
235
  'pre-commit': '# devsplain native pre-commit hook',
162
236
  'post-commit': '# devsplain native post-commit hook'
163
237
  };
164
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]
165
240
  let removed = 0;
166
- // Iterate through hook signatures to remove [ds]
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]
167
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]
168
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]
169
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]
170
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]
171
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]
172
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]
173
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]
174
256
  removed++;
175
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]
176
259
  console.log(`[devsplain] Skipping ${hookName}: not installed by devsplain.`);
177
260
  }
178
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]
179
263
  console.log(`[devsplain] No ${hookName} hook found at: ${hookPath}`);
180
264
  }
181
265
  }
182
266
 
183
- // Log removal of hooks [ds]
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]
184
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]
185
270
  console.log(`[devsplain] Successfully removed ${removed} hook(s).`);
186
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]
187
273
  console.log('[devsplain] No devsplain hooks were found to remove.');
188
274
  }
189
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]
190
277
  console.warn('Warning: Could not remove Git hooks (not inside a git repository or git command missing).');
191
278
  console.warn(e.message);
192
279
  }
193
280
  }
194
281
 
195
- // Run installHooks if this script is executed directly [ds]
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]
196
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]
197
285
  installHooks();
198
286
  }
199
- // Export installHooks and removeHooks functions [ds]
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]
200
288
  module.exports = { installHooks, removeHooks };