claude-git-hooks 2.4.1 → 2.6.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/CHANGELOG.md +364 -150
- package/README.md +128 -40
- package/bin/claude-hooks +436 -2
- package/lib/config.js +29 -0
- package/lib/hooks/pre-commit.js +2 -6
- package/lib/hooks/prepare-commit-msg.js +27 -4
- package/lib/utils/claude-client.js +261 -136
- package/lib/utils/file-operations.js +0 -102
- package/lib/utils/github-api.js +641 -0
- package/lib/utils/github-client.js +770 -0
- package/lib/utils/interactive-ui.js +314 -0
- package/lib/utils/mcp-setup.js +342 -0
- package/lib/utils/sanitize.js +180 -0
- package/lib/utils/task-id.js +441 -0
- package/lib/utils/which-command.js +225 -0
- package/package.json +10 -1
- package/templates/CREATE_GITHUB_PR.md +32 -0
- package/templates/config.github.example.json +51 -0
- package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +18 -1
- package/templates/presets/ai/preset.json +37 -37
- package/templates/settings.local.example.json +4 -0
|
@@ -22,6 +22,7 @@ import os from 'os';
|
|
|
22
22
|
import logger from './logger.js';
|
|
23
23
|
import config from '../config.js';
|
|
24
24
|
import { detectClaudeError, formatClaudeError, ClaudeErrorType } from './claude-diagnostics.js';
|
|
25
|
+
import { which } from './which-command.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Custom error for Claude client failures
|
|
@@ -40,9 +41,7 @@ class ClaudeClientError extends Error {
|
|
|
40
41
|
* Detect if running on Windows
|
|
41
42
|
* Why: Need to use 'wsl claude' instead of 'claude' on Windows
|
|
42
43
|
*/
|
|
43
|
-
const isWindows = () =>
|
|
44
|
-
return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
45
|
-
};
|
|
44
|
+
const isWindows = () => os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
46
45
|
|
|
47
46
|
/**
|
|
48
47
|
* Check if WSL is available on Windows
|
|
@@ -68,6 +67,7 @@ const isWSLAvailable = () => {
|
|
|
68
67
|
/**
|
|
69
68
|
* Get Claude command configuration for current platform
|
|
70
69
|
* Why: On Windows, try native Claude first, then WSL as fallback
|
|
70
|
+
* Node 24 Fix: Uses which() to resolve absolute paths, avoiding shell: true
|
|
71
71
|
*
|
|
72
72
|
* @returns {Object} { command, args } - Command and base arguments
|
|
73
73
|
* @throws {ClaudeClientError} If Claude not available on any method
|
|
@@ -75,42 +75,68 @@ const isWSLAvailable = () => {
|
|
|
75
75
|
const getClaudeCommand = () => {
|
|
76
76
|
if (isWindows()) {
|
|
77
77
|
// Try native Windows Claude first (e.g., installed via npm/scoop/choco)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
// Node 24: Use which() instead of execSync to get absolute path
|
|
79
|
+
const nativePath = which('claude');
|
|
80
|
+
if (nativePath) {
|
|
81
|
+
logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI', { path: nativePath });
|
|
82
|
+
return { command: nativePath, args: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logger.debug('claude-client - getClaudeCommand', 'Native Claude not found, trying WSL');
|
|
86
|
+
|
|
87
|
+
// Fallback to WSL
|
|
88
|
+
if (!isWSLAvailable()) {
|
|
89
|
+
throw new ClaudeClientError('Claude CLI not found. Install Claude CLI natively on Windows or via WSL', {
|
|
90
|
+
context: {
|
|
91
|
+
platform: 'Windows',
|
|
92
|
+
suggestions: [
|
|
93
|
+
'Native Windows: npm install -g @anthropic-ai/claude-cli',
|
|
94
|
+
'WSL: wsl --install, then install Claude in WSL'
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
// Check if Claude is available in WSL
|
|
101
|
+
// Node 24: Resolve wsl.exe absolute path to avoid shell: true
|
|
102
|
+
const wslPath = which('wsl');
|
|
103
|
+
if (wslPath) {
|
|
99
104
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
// Verify Claude exists in WSL
|
|
106
|
+
execSync(`"${wslPath}" claude --version`, { stdio: 'ignore', timeout: 5000 });
|
|
107
|
+
logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', { wslPath });
|
|
108
|
+
return { command: wslPath, args: ['claude'] };
|
|
103
109
|
} catch (wslError) {
|
|
104
|
-
throw new ClaudeClientError('Claude CLI not found in
|
|
110
|
+
throw new ClaudeClientError('Claude CLI not found in WSL', {
|
|
105
111
|
context: {
|
|
106
112
|
platform: 'Windows',
|
|
107
|
-
|
|
113
|
+
wslPath,
|
|
108
114
|
wslError: wslError.message
|
|
109
115
|
}
|
|
110
116
|
});
|
|
111
117
|
}
|
|
112
118
|
}
|
|
119
|
+
|
|
120
|
+
throw new ClaudeClientError('Claude CLI not found in Windows or WSL', {
|
|
121
|
+
context: {
|
|
122
|
+
platform: 'Windows',
|
|
123
|
+
suggestions: [
|
|
124
|
+
'Install Claude CLI: npm install -g @anthropic-ai/claude-cli',
|
|
125
|
+
'Or install WSL and Claude in WSL'
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Unix (Linux, macOS): Use which() to get absolute path
|
|
132
|
+
const claudePath = which('claude');
|
|
133
|
+
if (claudePath) {
|
|
134
|
+
logger.debug('claude-client - getClaudeCommand', 'Using Claude CLI', { path: claudePath });
|
|
135
|
+
return { command: claudePath, args: [] };
|
|
113
136
|
}
|
|
137
|
+
|
|
138
|
+
// Fallback to 'claude' if which fails (will error later if not found)
|
|
139
|
+
logger.debug('claude-client - getClaudeCommand', 'which() failed, using fallback', { command: 'claude' });
|
|
114
140
|
return { command: 'claude', args: [] };
|
|
115
141
|
};
|
|
116
142
|
|
|
@@ -125,127 +151,222 @@ const getClaudeCommand = () => {
|
|
|
125
151
|
* @returns {Promise<string>} Claude's response
|
|
126
152
|
* @throws {ClaudeClientError} If execution fails or times out
|
|
127
153
|
*/
|
|
128
|
-
const executeClaude = (prompt, { timeout = 120000 } = {}) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) => new Promise((resolve, reject) => {
|
|
155
|
+
// Get platform-specific command
|
|
156
|
+
const { command, args } = getClaudeCommand();
|
|
157
|
+
|
|
158
|
+
// Add allowed tools if specified (for MCP tools)
|
|
159
|
+
const finalArgs = [...args];
|
|
160
|
+
if (allowedTools.length > 0) {
|
|
161
|
+
// Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
|
|
162
|
+
finalArgs.push('--allowedTools', allowedTools.join(','));
|
|
163
|
+
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
'claude-client - executeClaude',
|
|
136
|
-
'Executing Claude CLI',
|
|
137
|
-
{ promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows() }
|
|
138
|
-
);
|
|
165
|
+
const fullCommand = finalArgs.length > 0 ? `${command} ${finalArgs.join(' ')}` : command;
|
|
139
166
|
|
|
140
|
-
|
|
167
|
+
logger.debug(
|
|
168
|
+
'claude-client - executeClaude',
|
|
169
|
+
'Executing Claude CLI',
|
|
170
|
+
{ promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows(), allowedTools }
|
|
171
|
+
);
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
// spawn streams data, exec buffers everything in memory
|
|
144
|
-
// shell: true needed on Windows to resolve .cmd/.bat executables
|
|
145
|
-
const claude = spawn(command, args, {
|
|
146
|
-
stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
|
|
147
|
-
shell: true // Required for Windows to find .cmd/.bat files
|
|
148
|
-
});
|
|
173
|
+
const startTime = Date.now();
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
// Why: Use spawn instead of exec to handle large prompts and responses
|
|
176
|
+
// spawn streams data, exec buffers everything in memory
|
|
177
|
+
// Node 24 Fix: Removed shell: true to avoid DEP0190 deprecation warning
|
|
178
|
+
// We now use absolute paths from which(), so shell is not needed
|
|
179
|
+
const claude = spawn(command, finalArgs, {
|
|
180
|
+
stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
|
|
181
|
+
});
|
|
152
182
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
stdout += data.toString();
|
|
156
|
-
});
|
|
183
|
+
let stdout = '';
|
|
184
|
+
let stderr = '';
|
|
157
185
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
186
|
+
// Collect stdout
|
|
187
|
+
claude.stdout.on('data', (data) => {
|
|
188
|
+
stdout += data.toString();
|
|
189
|
+
});
|
|
162
190
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (code === 0) {
|
|
168
|
-
logger.debug(
|
|
169
|
-
'claude-client - executeClaude',
|
|
170
|
-
'Claude CLI execution successful',
|
|
171
|
-
{ duration, outputLength: stdout.length }
|
|
172
|
-
);
|
|
173
|
-
resolve(stdout);
|
|
174
|
-
} else {
|
|
175
|
-
// Detect specific error type
|
|
176
|
-
const errorInfo = detectClaudeError(stdout, stderr, code);
|
|
177
|
-
|
|
178
|
-
logger.error(
|
|
179
|
-
'claude-client - executeClaude',
|
|
180
|
-
`Claude CLI failed: ${errorInfo.type}`,
|
|
181
|
-
new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
182
|
-
output: { stdout, stderr },
|
|
183
|
-
context: { exitCode: code, duration, errorType: errorInfo.type }
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// Show formatted error to user
|
|
188
|
-
const formattedError = formatClaudeError(errorInfo);
|
|
189
|
-
console.error('\n' + formattedError + '\n');
|
|
190
|
-
|
|
191
|
-
reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
192
|
-
output: { stdout, stderr },
|
|
193
|
-
context: { exitCode: code, duration, errorInfo }
|
|
194
|
-
}));
|
|
195
|
-
}
|
|
196
|
-
});
|
|
191
|
+
// Collect stderr
|
|
192
|
+
claude.stderr.on('data', (data) => {
|
|
193
|
+
stderr += data.toString();
|
|
194
|
+
});
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
// Handle process completion
|
|
197
|
+
claude.on('close', (code) => {
|
|
198
|
+
const duration = Date.now() - startTime;
|
|
199
|
+
|
|
200
|
+
if (code === 0) {
|
|
201
|
+
logger.debug(
|
|
201
202
|
'claude-client - executeClaude',
|
|
202
|
-
'
|
|
203
|
-
|
|
203
|
+
'Claude CLI execution successful',
|
|
204
|
+
{ duration, outputLength: stdout.length }
|
|
204
205
|
);
|
|
206
|
+
resolve(stdout);
|
|
207
|
+
} else {
|
|
208
|
+
// Detect specific error type
|
|
209
|
+
const errorInfo = detectClaudeError(stdout, stderr, code);
|
|
205
210
|
|
|
206
|
-
reject(new ClaudeClientError('Failed to spawn Claude CLI', {
|
|
207
|
-
cause: error,
|
|
208
|
-
context: { command, args }
|
|
209
|
-
}));
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Set up timeout
|
|
213
|
-
const timeoutId = setTimeout(() => {
|
|
214
|
-
claude.kill();
|
|
215
211
|
logger.error(
|
|
216
212
|
'claude-client - executeClaude',
|
|
217
|
-
|
|
218
|
-
new ClaudeClientError(
|
|
219
|
-
|
|
213
|
+
`Claude CLI failed: ${errorInfo.type}`,
|
|
214
|
+
new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
215
|
+
output: { stdout, stderr },
|
|
216
|
+
context: { exitCode: code, duration, errorType: errorInfo.type }
|
|
220
217
|
})
|
|
221
218
|
);
|
|
222
219
|
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
// Show formatted error to user
|
|
221
|
+
const formattedError = formatClaudeError(errorInfo);
|
|
222
|
+
console.error(`\n${ formattedError }\n`);
|
|
223
|
+
|
|
224
|
+
reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
225
|
+
output: { stdout, stderr },
|
|
226
|
+
context: { exitCode: code, duration, errorInfo }
|
|
225
227
|
}));
|
|
226
|
-
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
// Handle errors
|
|
232
|
+
claude.on('error', (error) => {
|
|
233
|
+
logger.error(
|
|
234
|
+
'claude-client - executeClaude',
|
|
235
|
+
'Failed to spawn Claude CLI process',
|
|
236
|
+
error
|
|
237
|
+
);
|
|
230
238
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
} catch (error) {
|
|
237
|
-
logger.error(
|
|
238
|
-
'claude-client - executeClaude',
|
|
239
|
-
'Failed to write prompt to Claude CLI stdin',
|
|
240
|
-
error
|
|
241
|
-
);
|
|
239
|
+
reject(new ClaudeClientError('Failed to spawn Claude CLI', {
|
|
240
|
+
cause: error,
|
|
241
|
+
context: { command, args }
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
242
244
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Set up timeout
|
|
246
|
+
const timeoutId = setTimeout(() => {
|
|
247
|
+
claude.kill();
|
|
248
|
+
logger.error(
|
|
249
|
+
'claude-client - executeClaude',
|
|
250
|
+
'Claude CLI execution timed out',
|
|
251
|
+
new ClaudeClientError('Claude CLI timeout', {
|
|
252
|
+
context: { timeout }
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
reject(new ClaudeClientError('Claude CLI execution timed out', {
|
|
257
|
+
context: { timeout }
|
|
258
|
+
}));
|
|
259
|
+
}, timeout);
|
|
260
|
+
|
|
261
|
+
// Clear timeout if process completes
|
|
262
|
+
claude.on('close', () => clearTimeout(timeoutId));
|
|
263
|
+
|
|
264
|
+
// Write prompt to stdin
|
|
265
|
+
// Why: Claude CLI reads prompt from stdin, not command arguments
|
|
266
|
+
try {
|
|
267
|
+
claude.stdin.write(prompt);
|
|
268
|
+
claude.stdin.end();
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger.error(
|
|
271
|
+
'claude-client - executeClaude',
|
|
272
|
+
'Failed to write prompt to Claude CLI stdin',
|
|
273
|
+
error
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
reject(new ClaudeClientError('Failed to write prompt', {
|
|
277
|
+
cause: error
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Executes Claude CLI fully interactively
|
|
284
|
+
* Why: Allows user to interact with Claude and approve MCP permissions
|
|
285
|
+
*
|
|
286
|
+
* @param {string} prompt - Prompt text to send to Claude
|
|
287
|
+
* @param {Object} options - Execution options
|
|
288
|
+
* @returns {Promise<string>} - Returns 'interactive' since we can't capture output
|
|
289
|
+
* @throws {ClaudeClientError} If execution fails
|
|
290
|
+
*/
|
|
291
|
+
const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Promise((resolve, reject) => {
|
|
292
|
+
const { command, args } = getClaudeCommand();
|
|
293
|
+
const { spawnSync } = require('child_process');
|
|
294
|
+
const fs = require('fs');
|
|
295
|
+
const path = require('path');
|
|
296
|
+
const os = require('os');
|
|
297
|
+
|
|
298
|
+
// Save prompt to temp file that Claude can read
|
|
299
|
+
const tempDir = os.tmpdir();
|
|
300
|
+
const tempFile = path.join(tempDir, 'claude-pr-instructions.md');
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
fs.writeFileSync(tempFile, prompt);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
logger.error('claude-client - executeClaudeInteractive', 'Failed to write temp file', err);
|
|
306
|
+
reject(new ClaudeClientError('Failed to write prompt file', { cause: err }));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
logger.debug(
|
|
311
|
+
'claude-client - executeClaudeInteractive',
|
|
312
|
+
'Starting interactive Claude session',
|
|
313
|
+
{ promptLength: prompt.length, tempFile, command, args }
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log('╔══════════════════════════════════════════════════════════════════╗');
|
|
318
|
+
console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║');
|
|
319
|
+
console.log('╠══════════════════════════════════════════════════════════════════╣');
|
|
320
|
+
console.log('║ ║');
|
|
321
|
+
console.log('║ Instructions saved to: ║');
|
|
322
|
+
console.log(`║ ${tempFile.padEnd(62)}║`);
|
|
323
|
+
console.log('║ ║');
|
|
324
|
+
console.log('║ When Claude starts, tell it: ║');
|
|
325
|
+
console.log('║ "Read and execute the instructions in the file above" ║');
|
|
326
|
+
console.log('║ ║');
|
|
327
|
+
console.log('║ • Type "y" if prompted for MCP permissions ║');
|
|
328
|
+
console.log('║ • Type "/exit" when done ║');
|
|
329
|
+
console.log('║ ║');
|
|
330
|
+
console.log('╚══════════════════════════════════════════════════════════════════╝');
|
|
331
|
+
console.log('');
|
|
332
|
+
console.log('Starting Claude...');
|
|
333
|
+
console.log('');
|
|
334
|
+
|
|
335
|
+
// Run Claude fully interactively (no flags - pure interactive mode)
|
|
336
|
+
const result = spawnSync(command, args, {
|
|
337
|
+
stdio: 'inherit', // Full terminal access
|
|
338
|
+
shell: true,
|
|
339
|
+
timeout
|
|
247
340
|
});
|
|
248
|
-
|
|
341
|
+
|
|
342
|
+
// Clean up temp file
|
|
343
|
+
try {
|
|
344
|
+
fs.unlinkSync(tempFile);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', { error: e.message });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (result.error) {
|
|
350
|
+
logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error);
|
|
351
|
+
reject(new ClaudeClientError('Failed to start Claude', { cause: result.error }));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (result.status === 0 || result.status === null) {
|
|
356
|
+
console.log('');
|
|
357
|
+
resolve('interactive-session-completed');
|
|
358
|
+
} else if (result.signal === 'SIGTERM') {
|
|
359
|
+
reject(new ClaudeClientError('Claude session timed out', { context: { timeout } }));
|
|
360
|
+
} else {
|
|
361
|
+
logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', {
|
|
362
|
+
status: result.status,
|
|
363
|
+
signal: result.signal
|
|
364
|
+
});
|
|
365
|
+
reject(new ClaudeClientError(`Claude exited with code ${result.status}`, {
|
|
366
|
+
context: { exitCode: result.status, signal: result.signal }
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
});
|
|
249
370
|
|
|
250
371
|
/**
|
|
251
372
|
* Extracts JSON from Claude's response
|
|
@@ -352,8 +473,8 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
|
|
|
352
473
|
timestamp: new Date().toISOString(),
|
|
353
474
|
promptLength: prompt.length,
|
|
354
475
|
responseLength: response.length,
|
|
355
|
-
prompt
|
|
356
|
-
response
|
|
476
|
+
prompt,
|
|
477
|
+
response
|
|
357
478
|
};
|
|
358
479
|
|
|
359
480
|
await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8');
|
|
@@ -361,12 +482,12 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
|
|
|
361
482
|
// Display batch optimization status
|
|
362
483
|
try {
|
|
363
484
|
if (prompt.includes('OPTIMIZATION')) {
|
|
364
|
-
console.log(
|
|
485
|
+
console.log(`\n${ '='.repeat(70)}`);
|
|
365
486
|
console.log('✅ BATCH OPTIMIZATION ENABLED');
|
|
366
487
|
console.log('='.repeat(70));
|
|
367
488
|
console.log('Multi-file analysis organized for efficient processing');
|
|
368
489
|
console.log('Check debug file for full prompt and response details');
|
|
369
|
-
console.log('='.repeat(70)
|
|
490
|
+
console.log(`${'='.repeat(70) }\n`);
|
|
370
491
|
}
|
|
371
492
|
} catch (parseError) {
|
|
372
493
|
// Ignore parsing errors, just skip the display
|
|
@@ -457,7 +578,7 @@ const chunkArray = (array, size) => {
|
|
|
457
578
|
const analyzeCodeParallel = async (prompts, options = {}) => {
|
|
458
579
|
const startTime = Date.now();
|
|
459
580
|
|
|
460
|
-
console.log(
|
|
581
|
+
console.log(`\n${ '='.repeat(70)}`);
|
|
461
582
|
console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
|
|
462
583
|
console.log('='.repeat(70));
|
|
463
584
|
|
|
@@ -469,14 +590,14 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
|
|
|
469
590
|
return analyzeCode(prompt, options);
|
|
470
591
|
});
|
|
471
592
|
|
|
472
|
-
console.log(
|
|
593
|
+
console.log(' ⏳ Waiting for all batches to complete...\n');
|
|
473
594
|
const results = await Promise.all(promises);
|
|
474
595
|
|
|
475
596
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
476
597
|
|
|
477
598
|
console.log('='.repeat(70));
|
|
478
599
|
console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
|
|
479
|
-
console.log('='.repeat(70)
|
|
600
|
+
console.log(`${'='.repeat(70) }\n`);
|
|
480
601
|
|
|
481
602
|
logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
|
|
482
603
|
return results;
|
|
@@ -485,9 +606,13 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
|
|
|
485
606
|
export {
|
|
486
607
|
ClaudeClientError,
|
|
487
608
|
executeClaude,
|
|
609
|
+
executeClaudeInteractive,
|
|
488
610
|
extractJSON,
|
|
489
611
|
saveDebugResponse,
|
|
490
612
|
analyzeCode,
|
|
491
613
|
analyzeCodeParallel,
|
|
492
|
-
chunkArray
|
|
614
|
+
chunkArray,
|
|
615
|
+
isWindows,
|
|
616
|
+
isWSLAvailable,
|
|
617
|
+
getClaudeCommand
|
|
493
618
|
};
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Key responsibilities:
|
|
6
6
|
* - Read files with size validation
|
|
7
|
-
* - Filter SKIP-ANALYSIS patterns from code
|
|
8
7
|
* - Validate file extensions
|
|
9
8
|
* - Check file sizes
|
|
10
9
|
*
|
|
@@ -15,7 +14,6 @@
|
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
16
|
import fs from 'fs/promises';
|
|
18
|
-
import fsSync from 'fs';
|
|
19
17
|
import path from 'path';
|
|
20
18
|
import logger from './logger.js';
|
|
21
19
|
|
|
@@ -120,85 +118,6 @@ const readFile = async (filePath, { maxSize = 100000, encoding = 'utf8' } = {})
|
|
|
120
118
|
}
|
|
121
119
|
};
|
|
122
120
|
|
|
123
|
-
/**
|
|
124
|
-
* Filters SKIP_ANALYSIS patterns from code content
|
|
125
|
-
* Why: Allows developers to exclude specific code from analysis
|
|
126
|
-
*
|
|
127
|
-
* ⚠️ KNOWN ISSUE (EXPERIMENTAL/BROKEN):
|
|
128
|
-
* This feature does NOT work reliably. The pre-commit hook analyzes git diff
|
|
129
|
-
* instead of full file content. Markers added in previous commits are NOT
|
|
130
|
-
* present in subsequent diffs, so they are not detected.
|
|
131
|
-
*
|
|
132
|
-
* Example of failure:
|
|
133
|
-
* - Commit 1: Add // SKIP_ANALYSIS_BLOCK markers
|
|
134
|
-
* - Commit 2: Modify line inside block
|
|
135
|
-
* - Result: Diff only shows modified line, NOT the markers → filter fails
|
|
136
|
-
*
|
|
137
|
-
* Supports two patterns:
|
|
138
|
-
* 1. Single line: // SKIP_ANALYSIS_LINE (excludes next line)
|
|
139
|
-
* 2. Block: // SKIP_ANALYSIS_BLOCK ... // SKIP_ANALYSIS_BLOCK (excludes block)
|
|
140
|
-
*
|
|
141
|
-
* @param {string} content - File content to filter
|
|
142
|
-
* @returns {string} Filtered content
|
|
143
|
-
*/
|
|
144
|
-
const filterSkipAnalysis = (content) => {
|
|
145
|
-
logger.debug(
|
|
146
|
-
'file-operations - filterSkipAnalysis',
|
|
147
|
-
'Filtering SKIP_ANALYSIS patterns',
|
|
148
|
-
{ originalLength: content.length }
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const lines = content.split('\n');
|
|
152
|
-
let skipNext = false;
|
|
153
|
-
let inSkipBlock = false;
|
|
154
|
-
|
|
155
|
-
// Why: Use map instead of filter to preserve line numbers for error reporting
|
|
156
|
-
// Empty lines maintain original line number mapping
|
|
157
|
-
const filteredLines = lines.map((line) => {
|
|
158
|
-
// IMPORTANT: Check specific pattern first (SKIP_ANALYSIS_BLOCK)
|
|
159
|
-
// before general pattern (SKIP_ANALYSIS_LINE) to avoid substring match issues
|
|
160
|
-
|
|
161
|
-
// Detect SKIP_ANALYSIS_BLOCK (toggle block state)
|
|
162
|
-
if (line.includes('// SKIP_ANALYSIS_BLOCK')) {
|
|
163
|
-
inSkipBlock = !inSkipBlock;
|
|
164
|
-
return ''; // Preserve line number
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Detect single-line SKIP_ANALYSIS_LINE (must check AFTER block check)
|
|
168
|
-
if (line.includes('// SKIP_ANALYSIS_LINE')) {
|
|
169
|
-
skipNext = true;
|
|
170
|
-
return ''; // Preserve line number
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Skip lines inside block
|
|
174
|
-
if (inSkipBlock) {
|
|
175
|
-
return '';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Skip next line after single SKIP_ANALYSIS_LINE
|
|
179
|
-
if (skipNext) {
|
|
180
|
-
skipNext = false;
|
|
181
|
-
return '';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return line;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const filtered = filteredLines.join('\n');
|
|
188
|
-
|
|
189
|
-
logger.debug(
|
|
190
|
-
'file-operations - filterSkipAnalysis',
|
|
191
|
-
'Filtering complete',
|
|
192
|
-
{
|
|
193
|
-
originalLength: content.length,
|
|
194
|
-
filteredLength: filtered.length,
|
|
195
|
-
linesRemoved: lines.length - filteredLines.filter(l => l !== '').length
|
|
196
|
-
}
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
return filtered;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
121
|
/**
|
|
203
122
|
* Validates file extension against allowed list
|
|
204
123
|
* Why: Pre-commit hook should only analyze specific file types
|
|
@@ -361,32 +280,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
361
280
|
return fileMetadata;
|
|
362
281
|
};
|
|
363
282
|
|
|
364
|
-
/**
|
|
365
|
-
* Reads multiple files in parallel with filtering
|
|
366
|
-
* Why: Efficiently loads file contents for analysis
|
|
367
|
-
*
|
|
368
|
-
* @param {Array<string>} files - Array of file paths
|
|
369
|
-
* @param {Object} options - Read options
|
|
370
|
-
* @param {number} options.maxSize - Max file size
|
|
371
|
-
* @param {boolean} options.applySkipFilter - Apply SKIP-ANALYSIS filtering
|
|
372
|
-
* @returns {Promise<Array<Object>>} Files with content
|
|
373
|
-
*
|
|
374
|
-
* Returned object structure:
|
|
375
|
-
* [
|
|
376
|
-
* {
|
|
377
|
-
* path: string, // File path
|
|
378
|
-
* content: string, // File content (filtered if requested)
|
|
379
|
-
* size: number, // Original size in bytes
|
|
380
|
-
* error: Error // Error if read failed
|
|
381
|
-
* }
|
|
382
|
-
* ]
|
|
383
|
-
*/
|
|
384
283
|
export {
|
|
385
284
|
FileOperationError,
|
|
386
285
|
getFileSize,
|
|
387
286
|
fileExists,
|
|
388
287
|
readFile,
|
|
389
|
-
filterSkipAnalysis,
|
|
390
288
|
hasAllowedExtension,
|
|
391
289
|
filterFiles
|
|
392
290
|
};
|