claude-git-hooks 2.1.0 → 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/CHANGELOG.md +178 -0
- package/README.md +203 -79
- package/bin/claude-hooks +295 -119
- package/lib/config.js +163 -0
- package/lib/hooks/pre-commit.js +179 -67
- package/lib/hooks/prepare-commit-msg.js +47 -41
- package/lib/utils/claude-client.js +93 -11
- package/lib/utils/file-operations.js +1 -65
- package/lib/utils/file-utils.js +65 -0
- package/lib/utils/package-info.js +75 -0
- package/lib/utils/preset-loader.js +209 -0
- package/lib/utils/prompt-builder.js +83 -67
- package/lib/utils/resolution-prompt.js +12 -2
- package/package.json +49 -50
- package/templates/ANALYZE_DIFF.md +33 -0
- package/templates/COMMIT_MESSAGE.md +24 -0
- package/templates/SUBAGENT_INSTRUCTION.md +1 -0
- package/templates/config.example.json +41 -0
- package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
- package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
- package/templates/presets/ai/config.json +12 -0
- package/templates/presets/ai/preset.json +42 -0
- package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
- package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
- package/templates/presets/backend/config.json +12 -0
- package/templates/presets/backend/preset.json +49 -0
- package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
- package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
- package/templates/presets/database/config.json +12 -0
- package/templates/presets/database/preset.json +38 -0
- package/templates/presets/default/config.json +12 -0
- package/templates/presets/default/preset.json +53 -0
- package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
- package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
- package/templates/presets/frontend/config.json +12 -0
- package/templates/presets/frontend/preset.json +50 -0
- package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
- package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
- package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
- package/templates/presets/fullstack/config.json +12 -0
- package/templates/presets/fullstack/preset.json +55 -0
- package/templates/shared/ANALYSIS_PROMPT.md +103 -0
- package/templates/shared/ANALYZE_DIFF.md +33 -0
- package/templates/shared/COMMIT_MESSAGE.md +24 -0
- package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
- package/templates/shared/RESOLUTION_PROMPT.md +32 -0
- package/templates/check-version.sh +0 -266
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawn, execSync } from 'child_process';
|
|
18
18
|
import fs from 'fs/promises';
|
|
19
|
+
import path from 'path';
|
|
19
20
|
import os from 'os';
|
|
20
21
|
import logger from './logger.js';
|
|
22
|
+
import config from '../config.js';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Custom error for Claude client failures
|
|
@@ -53,7 +55,7 @@ const isWSLAvailable = () => {
|
|
|
53
55
|
|
|
54
56
|
try {
|
|
55
57
|
// Try to run wsl --version to check if WSL is installed
|
|
56
|
-
execSync('wsl --version', { stdio: 'ignore', timeout:
|
|
58
|
+
execSync('wsl --version', { stdio: 'ignore', timeout: config.system.wslCheckTimeout });
|
|
57
59
|
return true;
|
|
58
60
|
} catch (error) {
|
|
59
61
|
logger.debug('claude-client - isWSLAvailable', 'WSL not available', error);
|
|
@@ -294,15 +296,45 @@ const extractJSON = (response) => {
|
|
|
294
296
|
};
|
|
295
297
|
|
|
296
298
|
/**
|
|
297
|
-
* Saves response to debug file
|
|
298
|
-
* Why: Helps troubleshoot issues with Claude responses
|
|
299
|
+
* Saves prompt and response to debug file
|
|
300
|
+
* Why: Helps troubleshoot issues with Claude responses and verify prompts
|
|
299
301
|
*
|
|
300
|
-
* @param {string}
|
|
301
|
-
* @param {string}
|
|
302
|
+
* @param {string} prompt - Prompt sent to Claude
|
|
303
|
+
* @param {string} response - Raw response from Claude
|
|
304
|
+
* @param {string} filename - Debug filename (default: from config)
|
|
302
305
|
*/
|
|
303
|
-
const saveDebugResponse = async (response, filename =
|
|
306
|
+
const saveDebugResponse = async (prompt, response, filename = config.output.debugFile) => {
|
|
304
307
|
try {
|
|
305
|
-
|
|
308
|
+
// Ensure output directory exists
|
|
309
|
+
const outputDir = path.dirname(filename);
|
|
310
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
311
|
+
|
|
312
|
+
// Save full debug information
|
|
313
|
+
const debugData = {
|
|
314
|
+
timestamp: new Date().toISOString(),
|
|
315
|
+
promptLength: prompt.length,
|
|
316
|
+
responseLength: response.length,
|
|
317
|
+
prompt: prompt,
|
|
318
|
+
response: response
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8');
|
|
322
|
+
|
|
323
|
+
// Display batch optimization status
|
|
324
|
+
try {
|
|
325
|
+
if (prompt.includes('OPTIMIZATION')) {
|
|
326
|
+
console.log('\n' + '='.repeat(70));
|
|
327
|
+
console.log('✅ BATCH OPTIMIZATION ENABLED');
|
|
328
|
+
console.log('='.repeat(70));
|
|
329
|
+
console.log('Multi-file analysis organized for efficient processing');
|
|
330
|
+
console.log('Check debug file for full prompt and response details');
|
|
331
|
+
console.log('='.repeat(70) + '\n');
|
|
332
|
+
}
|
|
333
|
+
} catch (parseError) {
|
|
334
|
+
// Ignore parsing errors, just skip the display
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
logger.info(`📝 Debug output saved to ${filename}`);
|
|
306
338
|
logger.debug(
|
|
307
339
|
'claude-client - saveDebugResponse',
|
|
308
340
|
`Debug response saved to ${filename}`
|
|
@@ -323,11 +355,11 @@ const saveDebugResponse = async (response, filename = 'debug-claude-response.jso
|
|
|
323
355
|
* @param {string} prompt - Analysis prompt
|
|
324
356
|
* @param {Object} options - Analysis options
|
|
325
357
|
* @param {number} options.timeout - Timeout in milliseconds
|
|
326
|
-
* @param {boolean} options.saveDebug - Save response to debug file (default: from
|
|
358
|
+
* @param {boolean} options.saveDebug - Save response to debug file (default: from config)
|
|
327
359
|
* @returns {Promise<Object>} Parsed analysis result
|
|
328
360
|
* @throws {ClaudeClientError} If analysis fails
|
|
329
361
|
*/
|
|
330
|
-
const analyzeCode = async (prompt, { timeout = 120000, saveDebug =
|
|
362
|
+
const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug } = {}) => {
|
|
331
363
|
logger.debug(
|
|
332
364
|
'claude-client - analyzeCode',
|
|
333
365
|
'Starting code analysis',
|
|
@@ -340,7 +372,7 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
|
|
|
340
372
|
|
|
341
373
|
// Save debug if requested
|
|
342
374
|
if (saveDebug) {
|
|
343
|
-
await saveDebugResponse(response);
|
|
375
|
+
await saveDebugResponse(prompt, response);
|
|
344
376
|
}
|
|
345
377
|
|
|
346
378
|
// Extract and parse JSON
|
|
@@ -364,10 +396,60 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
|
|
|
364
396
|
}
|
|
365
397
|
};
|
|
366
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Splits array into chunks
|
|
401
|
+
* @param {Array} array - Array to split
|
|
402
|
+
* @param {number} size - Chunk size
|
|
403
|
+
* @returns {Array<Array>} Array of chunks
|
|
404
|
+
*/
|
|
405
|
+
const chunkArray = (array, size) => {
|
|
406
|
+
const chunks = [];
|
|
407
|
+
for (let i = 0; i < array.length; i += size) {
|
|
408
|
+
chunks.push(array.slice(i, i + size));
|
|
409
|
+
}
|
|
410
|
+
return chunks;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Runs multiple analyzeCode calls in parallel
|
|
415
|
+
* @param {Array<string>} prompts - Array of prompts to analyze
|
|
416
|
+
* @param {Object} options - Same options as analyzeCode
|
|
417
|
+
* @returns {Promise<Array<Object>>} Array of results
|
|
418
|
+
*/
|
|
419
|
+
const analyzeCodeParallel = async (prompts, options = {}) => {
|
|
420
|
+
const startTime = Date.now();
|
|
421
|
+
|
|
422
|
+
console.log('\n' + '='.repeat(70));
|
|
423
|
+
console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
|
|
424
|
+
console.log('='.repeat(70));
|
|
425
|
+
|
|
426
|
+
logger.info(`Starting parallel analysis: ${prompts.length} prompts`);
|
|
427
|
+
|
|
428
|
+
const promises = prompts.map((prompt, index) => {
|
|
429
|
+
console.log(` ⚡ Launching batch ${index + 1}/${prompts.length}...`);
|
|
430
|
+
logger.debug('claude-client - analyzeCodeParallel', `Starting batch ${index + 1}`);
|
|
431
|
+
return analyzeCode(prompt, options);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
console.log(` ⏳ Waiting for all batches to complete...\n`);
|
|
435
|
+
const results = await Promise.all(promises);
|
|
436
|
+
|
|
437
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
438
|
+
|
|
439
|
+
console.log('='.repeat(70));
|
|
440
|
+
console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
|
|
441
|
+
console.log('='.repeat(70) + '\n');
|
|
442
|
+
|
|
443
|
+
logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
|
|
444
|
+
return results;
|
|
445
|
+
};
|
|
446
|
+
|
|
367
447
|
export {
|
|
368
448
|
ClaudeClientError,
|
|
369
449
|
executeClaude,
|
|
370
450
|
extractJSON,
|
|
371
451
|
saveDebugResponse,
|
|
372
|
-
analyzeCode
|
|
452
|
+
analyzeCode,
|
|
453
|
+
analyzeCodeParallel,
|
|
454
|
+
chunkArray
|
|
373
455
|
};
|
|
@@ -347,69 +347,6 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
347
347
|
* }
|
|
348
348
|
* ]
|
|
349
349
|
*/
|
|
350
|
-
const readMultipleFiles = async (files, { maxSize = 100000, applySkipFilter = true } = {}) => {
|
|
351
|
-
logger.debug(
|
|
352
|
-
'file-operations - readMultipleFiles',
|
|
353
|
-
'Reading multiple files',
|
|
354
|
-
{ fileCount: files.length, maxSize, applySkipFilter }
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
const results = await Promise.allSettled(
|
|
358
|
-
files.map(async (filePath) => {
|
|
359
|
-
try {
|
|
360
|
-
let content = await readFile(filePath, { maxSize });
|
|
361
|
-
|
|
362
|
-
// Apply SKIP-ANALYSIS filtering if requested
|
|
363
|
-
if (applySkipFilter) {
|
|
364
|
-
content = filterSkipAnalysis(content);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const size = await getFileSize(filePath);
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
path: filePath,
|
|
371
|
-
content,
|
|
372
|
-
size,
|
|
373
|
-
error: null
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
} catch (error) {
|
|
377
|
-
logger.error(
|
|
378
|
-
'file-operations - readMultipleFiles',
|
|
379
|
-
`Failed to read file: ${filePath}`,
|
|
380
|
-
error
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
path: filePath,
|
|
385
|
-
content: null,
|
|
386
|
-
size: 0,
|
|
387
|
-
error
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
// Extract all results (both successful and failed)
|
|
394
|
-
const fileContents = results.map(r =>
|
|
395
|
-
r.status === 'fulfilled' ? r.value : r.reason
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
const successCount = fileContents.filter(f => f.error === null).length;
|
|
399
|
-
|
|
400
|
-
logger.debug(
|
|
401
|
-
'file-operations - readMultipleFiles',
|
|
402
|
-
'Reading complete',
|
|
403
|
-
{
|
|
404
|
-
totalFiles: files.length,
|
|
405
|
-
successfulReads: successCount,
|
|
406
|
-
failedReads: files.length - successCount
|
|
407
|
-
}
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
return fileContents;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
350
|
export {
|
|
414
351
|
FileOperationError,
|
|
415
352
|
getFileSize,
|
|
@@ -417,6 +354,5 @@ export {
|
|
|
417
354
|
readFile,
|
|
418
355
|
filterSkipAnalysis,
|
|
419
356
|
hasAllowedExtension,
|
|
420
|
-
filterFiles
|
|
421
|
-
readMultipleFiles
|
|
357
|
+
filterFiles
|
|
422
358
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: file-utils.js
|
|
3
|
+
* Purpose: Utility functions for file system operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import fsSync from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { getRepoRoot } from './git-operations.js';
|
|
10
|
+
import logger from './logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensures a directory exists (creates if not present)
|
|
14
|
+
*
|
|
15
|
+
* @param {string} dirPath - Directory path to ensure
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
export const ensureDir = async (dirPath) => {
|
|
19
|
+
const absolutePath = path.isAbsolute(dirPath)
|
|
20
|
+
? dirPath
|
|
21
|
+
: path.join(getRepoRoot(), dirPath);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await fs.mkdir(absolutePath, { recursive: true });
|
|
25
|
+
logger.debug('file-utils - ensureDir', 'Directory ensured', { path: absolutePath });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
logger.error('file-utils - ensureDir', 'Failed to create directory', error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensures the output directory exists before writing files
|
|
34
|
+
* Creates .claude/out/ if it doesn't exist
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} config - Configuration object with output.outputDir
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
export const ensureOutputDir = async (config) => {
|
|
40
|
+
const outputDir = config?.output?.outputDir || '.claude/out';
|
|
41
|
+
await ensureDir(outputDir);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Writes a file ensuring its directory exists
|
|
46
|
+
*
|
|
47
|
+
* @param {string} filePath - File path to write
|
|
48
|
+
* @param {string} content - File content
|
|
49
|
+
* @param {Object} config - Configuration object
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
export const writeOutputFile = async (filePath, content, config) => {
|
|
53
|
+
await ensureOutputDir(config);
|
|
54
|
+
|
|
55
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
56
|
+
? filePath
|
|
57
|
+
: path.join(getRepoRoot(), filePath);
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(absolutePath, content, 'utf8');
|
|
60
|
+
|
|
61
|
+
logger.debug('file-utils - writeOutputFile', 'File written', {
|
|
62
|
+
path: absolutePath,
|
|
63
|
+
size: content.length
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: package-info.js
|
|
3
|
+
* Purpose: Utility for reading package.json information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} PackageData
|
|
15
|
+
* @property {string} name - Package name
|
|
16
|
+
* @property {string} version - Package version
|
|
17
|
+
* @property {string} [description] - Package description
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cache for package.json to avoid repeated file reads
|
|
22
|
+
* @type {PackageData|undefined}
|
|
23
|
+
*/
|
|
24
|
+
let packageCache;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets package.json data with caching (async)
|
|
28
|
+
* @returns {Promise<PackageData>} Package data with name and version
|
|
29
|
+
* @throws {Error} If package.json cannot be read
|
|
30
|
+
*/
|
|
31
|
+
export const getPackageJson = async () => {
|
|
32
|
+
// Return cached value if available
|
|
33
|
+
if (packageCache) {
|
|
34
|
+
return packageCache;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
39
|
+
const content = await fs.readFile(packagePath, 'utf8');
|
|
40
|
+
packageCache = JSON.parse(content);
|
|
41
|
+
return packageCache;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Failed to read package.json: ${error.message}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets package version (async)
|
|
49
|
+
* @returns {Promise<string>} Package version
|
|
50
|
+
*/
|
|
51
|
+
export const getVersion = async () => {
|
|
52
|
+
const pkg = await getPackageJson();
|
|
53
|
+
return pkg.version;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets package name (async)
|
|
58
|
+
* @returns {Promise<string>} Package name
|
|
59
|
+
*/
|
|
60
|
+
export const getPackageName = async () => {
|
|
61
|
+
const pkg = await getPackageJson();
|
|
62
|
+
return pkg.name;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calculates batch information for subagent processing
|
|
67
|
+
* @param {number} fileCount - Number of files to process
|
|
68
|
+
* @param {number} batchSize - Size of each batch
|
|
69
|
+
* @returns {Object} Batch information { numBatches, shouldShowBatches }
|
|
70
|
+
*/
|
|
71
|
+
export const calculateBatches = (fileCount, batchSize) => {
|
|
72
|
+
const numBatches = Math.ceil(fileCount / batchSize);
|
|
73
|
+
const shouldShowBatches = fileCount > batchSize;
|
|
74
|
+
return { numBatches, shouldShowBatches };
|
|
75
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: preset-loader.js
|
|
3
|
+
* Purpose: Loads preset configurations and metadata
|
|
4
|
+
*
|
|
5
|
+
* Key responsibilities:
|
|
6
|
+
* - Load preset.json (metadata)
|
|
7
|
+
* - Load config.json (overrides)
|
|
8
|
+
* - Resolve template paths
|
|
9
|
+
* - Replace placeholders in templates
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - fs/promises: Async file operations
|
|
13
|
+
* - path: Cross-platform path handling
|
|
14
|
+
* - git-operations: For getRepoRoot()
|
|
15
|
+
* - logger: Debug and error logging
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { getRepoRoot } from './git-operations.js';
|
|
21
|
+
import logger from './logger.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom error for preset loading failures
|
|
25
|
+
*/
|
|
26
|
+
class PresetError extends Error {
|
|
27
|
+
constructor(message, { presetName, cause } = {}) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'PresetError';
|
|
30
|
+
this.presetName = presetName;
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Loads preset metadata + config
|
|
37
|
+
* @param {string} presetName - Name of preset (backend, frontend, etc.)
|
|
38
|
+
* @returns {Promise<Object>} { metadata, config, templates }
|
|
39
|
+
*/
|
|
40
|
+
export async function loadPreset(presetName) {
|
|
41
|
+
logger.debug(
|
|
42
|
+
'preset-loader - loadPreset',
|
|
43
|
+
'Loading preset',
|
|
44
|
+
{ presetName }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const repoRoot = getRepoRoot();
|
|
48
|
+
|
|
49
|
+
// Only try .claude/presets/{name} (installed by claude-hooks install)
|
|
50
|
+
const presetDir = path.join(repoRoot, '.claude', 'presets', presetName);
|
|
51
|
+
|
|
52
|
+
logger.debug('preset-loader - loadPreset', 'Loading preset from', { presetDir });
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Load preset.json (metadata)
|
|
56
|
+
const presetJsonPath = path.join(presetDir, 'preset.json');
|
|
57
|
+
const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
|
|
58
|
+
const metadata = JSON.parse(metadataRaw);
|
|
59
|
+
|
|
60
|
+
// Load config.json (overrides)
|
|
61
|
+
const configJsonPath = path.join(presetDir, 'config.json');
|
|
62
|
+
const configRaw = await fs.readFile(configJsonPath, 'utf8');
|
|
63
|
+
const config = JSON.parse(configRaw);
|
|
64
|
+
|
|
65
|
+
// Resolve template paths
|
|
66
|
+
const templates = {};
|
|
67
|
+
for (const [key, templatePath] of Object.entries(metadata.templates)) {
|
|
68
|
+
templates[key] = path.join(presetDir, templatePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.debug(
|
|
72
|
+
'preset-loader - loadPreset',
|
|
73
|
+
'Preset loaded successfully',
|
|
74
|
+
{
|
|
75
|
+
presetName,
|
|
76
|
+
displayName: metadata.displayName,
|
|
77
|
+
fileExtensions: metadata.fileExtensions,
|
|
78
|
+
templateCount: Object.keys(templates).length
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return { metadata, config, templates };
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error(
|
|
86
|
+
'preset-loader - loadPreset',
|
|
87
|
+
`Failed to load preset: ${presetName}`,
|
|
88
|
+
error
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
throw new PresetError(`Preset "${presetName}" not found or invalid`, {
|
|
92
|
+
presetName,
|
|
93
|
+
cause: error
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Loads a template file and replaces placeholders
|
|
100
|
+
* @param {string} templatePath - Absolute path to template file
|
|
101
|
+
* @param {Object} metadata - Preset metadata for placeholder replacement
|
|
102
|
+
* @returns {Promise<string>} Template content with placeholders replaced
|
|
103
|
+
*/
|
|
104
|
+
export async function loadTemplate(templatePath, metadata) {
|
|
105
|
+
logger.debug(
|
|
106
|
+
'preset-loader - loadTemplate',
|
|
107
|
+
'Loading template',
|
|
108
|
+
{ templatePath }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
let template = await fs.readFile(templatePath, 'utf8');
|
|
113
|
+
|
|
114
|
+
// Replace placeholders
|
|
115
|
+
template = template.replace(
|
|
116
|
+
/{{TECH_STACK}}/g,
|
|
117
|
+
metadata.techStack.join(', ')
|
|
118
|
+
);
|
|
119
|
+
template = template.replace(
|
|
120
|
+
/{{FOCUS_AREAS}}/g,
|
|
121
|
+
metadata.focusAreas.join(', ')
|
|
122
|
+
);
|
|
123
|
+
template = template.replace(
|
|
124
|
+
/{{FILE_EXTENSIONS}}/g,
|
|
125
|
+
metadata.fileExtensions.join(', ')
|
|
126
|
+
);
|
|
127
|
+
template = template.replace(
|
|
128
|
+
/{{PRESET_NAME}}/g,
|
|
129
|
+
metadata.name
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
logger.debug(
|
|
133
|
+
'preset-loader - loadTemplate',
|
|
134
|
+
'Template loaded and processed',
|
|
135
|
+
{
|
|
136
|
+
templatePath,
|
|
137
|
+
originalLength: template.length
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return template;
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error(
|
|
145
|
+
'preset-loader - loadTemplate',
|
|
146
|
+
`Failed to load template: ${templatePath}`,
|
|
147
|
+
error
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
throw new PresetError(`Template not found: ${templatePath}`, {
|
|
151
|
+
cause: error
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Lists all available presets in .claude/presets/
|
|
158
|
+
* @returns {Promise<Array<Object>>} Array of preset metadata
|
|
159
|
+
* Returns: [{ name, displayName, description }]
|
|
160
|
+
*/
|
|
161
|
+
export async function listPresets() {
|
|
162
|
+
logger.debug('preset-loader - listPresets', 'Listing available presets');
|
|
163
|
+
|
|
164
|
+
const repoRoot = getRepoRoot();
|
|
165
|
+
const presets = [];
|
|
166
|
+
|
|
167
|
+
// Load all presets from .claude/presets/ (installed by claude-hooks install)
|
|
168
|
+
const presetsDir = path.join(repoRoot, '.claude', 'presets');
|
|
169
|
+
try {
|
|
170
|
+
const presetNames = await fs.readdir(presetsDir);
|
|
171
|
+
|
|
172
|
+
for (const name of presetNames) {
|
|
173
|
+
const presetJsonPath = path.join(presetsDir, name, 'preset.json');
|
|
174
|
+
try {
|
|
175
|
+
const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
|
|
176
|
+
const metadata = JSON.parse(metadataRaw);
|
|
177
|
+
|
|
178
|
+
presets.push({
|
|
179
|
+
name: metadata.name,
|
|
180
|
+
displayName: metadata.displayName,
|
|
181
|
+
description: metadata.description
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.debug(
|
|
185
|
+
'preset-loader - listPresets',
|
|
186
|
+
`Skipping invalid preset: ${name}`,
|
|
187
|
+
error
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.warning('No presets directory found. Run "claude-hooks install" first.');
|
|
193
|
+
logger.debug(
|
|
194
|
+
'preset-loader - listPresets',
|
|
195
|
+
'Failed to read presets directory',
|
|
196
|
+
error
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
logger.debug(
|
|
201
|
+
'preset-loader - listPresets',
|
|
202
|
+
'Presets listed',
|
|
203
|
+
{ count: presets.length }
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return presets;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export { PresetError };
|