claude-git-hooks 2.1.0 → 2.3.1
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 +240 -0
- package/README.md +280 -78
- package/bin/claude-hooks +295 -119
- package/lib/config.js +164 -0
- package/lib/hooks/pre-commit.js +180 -67
- package/lib/hooks/prepare-commit-msg.js +47 -41
- package/lib/utils/claude-client.js +107 -16
- package/lib/utils/claude-diagnostics.js +266 -0
- package/lib/utils/file-operations.js +1 -65
- package/lib/utils/file-utils.js +65 -0
- package/lib/utils/installation-diagnostics.js +145 -0
- package/lib/utils/package-info.js +75 -0
- package/lib/utils/preset-loader.js +214 -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/CUSTOMIZATION_GUIDE.md +656 -0
- package/templates/SUBAGENT_INSTRUCTION.md +1 -0
- package/templates/config.example.json +41 -0
- package/templates/pre-commit +40 -2
- package/templates/prepare-commit-msg +40 -2
- 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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: installation-diagnostics.js
|
|
3
|
+
* Purpose: Reusable error diagnostics and formatting utilities
|
|
4
|
+
*
|
|
5
|
+
* Key features:
|
|
6
|
+
* - Generic error formatting with installation diagnostics
|
|
7
|
+
* - Detects common installation issues
|
|
8
|
+
* - Provides actionable remediation steps
|
|
9
|
+
* - Extensible for future diagnostic checks
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { formatError } from './installation-diagnostics.js';
|
|
13
|
+
*
|
|
14
|
+
* try {
|
|
15
|
+
* // ... operation that might fail
|
|
16
|
+
* } catch (error) {
|
|
17
|
+
* console.error(formatError('Presets not found'));
|
|
18
|
+
* process.exit(1);
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { getRepoRoot } from './git-operations.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets installation diagnostics
|
|
28
|
+
* Why: Centralized logic to detect common installation issues
|
|
29
|
+
*
|
|
30
|
+
* Future enhancements:
|
|
31
|
+
* - Check file permissions
|
|
32
|
+
* - Verify Claude CLI installation
|
|
33
|
+
* - Check Node.js version compatibility
|
|
34
|
+
* - Validate .gitignore entries
|
|
35
|
+
* - Check hook file integrity
|
|
36
|
+
* - Verify template files exist
|
|
37
|
+
* - Check config.json validity
|
|
38
|
+
*
|
|
39
|
+
* @returns {Object} Diagnostic information
|
|
40
|
+
*/
|
|
41
|
+
export const getInstallationDiagnostics = () => {
|
|
42
|
+
const diagnostics = {
|
|
43
|
+
currentDir: process.cwd(),
|
|
44
|
+
repoRoot: null,
|
|
45
|
+
isInRepoRoot: false,
|
|
46
|
+
claudeDirExists: false,
|
|
47
|
+
claudeDirPath: null,
|
|
48
|
+
presetsDirExists: false,
|
|
49
|
+
presetsDirPath: null,
|
|
50
|
+
gitHooksExists: false,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
diagnostics.repoRoot = getRepoRoot();
|
|
55
|
+
diagnostics.isInRepoRoot = diagnostics.currentDir === diagnostics.repoRoot;
|
|
56
|
+
|
|
57
|
+
diagnostics.claudeDirPath = path.join(diagnostics.repoRoot, '.claude');
|
|
58
|
+
diagnostics.claudeDirExists = fs.existsSync(diagnostics.claudeDirPath);
|
|
59
|
+
|
|
60
|
+
diagnostics.presetsDirPath = path.join(diagnostics.claudeDirPath, 'presets');
|
|
61
|
+
diagnostics.presetsDirExists = fs.existsSync(diagnostics.presetsDirPath);
|
|
62
|
+
|
|
63
|
+
const gitHooksPath = path.join(diagnostics.repoRoot, '.git', 'hooks');
|
|
64
|
+
diagnostics.gitHooksExists = fs.existsSync(gitHooksPath);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Not in a git repository - diagnostics.repoRoot will be null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return diagnostics;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats error message with diagnostics and remediation steps
|
|
74
|
+
* Why: Provides consistent, actionable error messages across all errors
|
|
75
|
+
*
|
|
76
|
+
* @param {string} errorMessage - Description of what failed (e.g., "Presets not found", "Template file missing")
|
|
77
|
+
* @param {string[]} additionalContext - Optional additional context lines
|
|
78
|
+
* @returns {string} Formatted error message with diagnostics and remediation steps
|
|
79
|
+
*/
|
|
80
|
+
export const formatError = (errorMessage, additionalContext = []) => {
|
|
81
|
+
const diagnostics = getInstallationDiagnostics();
|
|
82
|
+
const lines = [];
|
|
83
|
+
|
|
84
|
+
lines.push(`⚠️ ${errorMessage}`);
|
|
85
|
+
lines.push('');
|
|
86
|
+
|
|
87
|
+
// Add any additional context first
|
|
88
|
+
if (additionalContext.length > 0) {
|
|
89
|
+
lines.push(...additionalContext);
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Diagnostic information
|
|
94
|
+
lines.push('Installation diagnostics:');
|
|
95
|
+
lines.push(` Current directory: ${diagnostics.currentDir}`);
|
|
96
|
+
if (diagnostics.repoRoot) {
|
|
97
|
+
lines.push(` Repository root: ${diagnostics.repoRoot}`);
|
|
98
|
+
lines.push(` .claude/ exists: ${diagnostics.claudeDirExists ? '✓' : '✗'}`);
|
|
99
|
+
lines.push(` presets/ exists: ${diagnostics.presetsDirExists ? '✓' : '✗'}`);
|
|
100
|
+
} else {
|
|
101
|
+
lines.push(` Repository root: [Not in a git repository]`);
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
// Remediation steps based on detected issues
|
|
106
|
+
lines.push('Recommended solution:');
|
|
107
|
+
if (!diagnostics.repoRoot) {
|
|
108
|
+
lines.push(' Not in a git repository');
|
|
109
|
+
lines.push(' → Navigate to your repository and try again');
|
|
110
|
+
} else if (!diagnostics.claudeDirExists) {
|
|
111
|
+
lines.push(' claude-hooks not installed');
|
|
112
|
+
if (!diagnostics.isInRepoRoot) {
|
|
113
|
+
lines.push(` → cd ${diagnostics.repoRoot}`);
|
|
114
|
+
lines.push(' → claude-hooks install');
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(' → claude-hooks install');
|
|
117
|
+
}
|
|
118
|
+
} else if (!diagnostics.isInRepoRoot) {
|
|
119
|
+
lines.push(' Running from subdirectory (may cause path issues)');
|
|
120
|
+
lines.push(` → cd ${diagnostics.repoRoot}`);
|
|
121
|
+
lines.push(' → claude-hooks uninstall');
|
|
122
|
+
lines.push(' → claude-hooks install');
|
|
123
|
+
} else if (!diagnostics.presetsDirExists) {
|
|
124
|
+
lines.push(' Incomplete installation (presets missing)');
|
|
125
|
+
lines.push(' → claude-hooks install --force');
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(' Unknown issue detected');
|
|
128
|
+
lines.push(' → claude-hooks install --force');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks if installation appears healthy
|
|
136
|
+
* Why: Quick validation before operations that require full installation
|
|
137
|
+
*
|
|
138
|
+
* @returns {boolean} True if installation looks healthy
|
|
139
|
+
*/
|
|
140
|
+
export const isInstallationHealthy = () => {
|
|
141
|
+
const diagnostics = getInstallationDiagnostics();
|
|
142
|
+
return diagnostics.claudeDirExists &&
|
|
143
|
+
diagnostics.presetsDirExists &&
|
|
144
|
+
diagnostics.gitHooksExists;
|
|
145
|
+
};
|
|
@@ -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,214 @@
|
|
|
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
|
+
* - installation-diagnostics: Error formatting with remediation steps
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs/promises';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { getRepoRoot } from './git-operations.js';
|
|
22
|
+
import logger from './logger.js';
|
|
23
|
+
import { formatError } from './installation-diagnostics.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom error for preset loading failures
|
|
27
|
+
*/
|
|
28
|
+
class PresetError extends Error {
|
|
29
|
+
constructor(message, { presetName, cause } = {}) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'PresetError';
|
|
32
|
+
this.presetName = presetName;
|
|
33
|
+
this.cause = cause;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Loads preset metadata + config
|
|
39
|
+
* @param {string} presetName - Name of preset (backend, frontend, etc.)
|
|
40
|
+
* @returns {Promise<Object>} { metadata, config, templates }
|
|
41
|
+
*/
|
|
42
|
+
export async function loadPreset(presetName) {
|
|
43
|
+
logger.debug(
|
|
44
|
+
'preset-loader - loadPreset',
|
|
45
|
+
'Loading preset',
|
|
46
|
+
{ presetName }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const repoRoot = getRepoRoot();
|
|
50
|
+
|
|
51
|
+
// Only try .claude/presets/{name} (installed by claude-hooks install)
|
|
52
|
+
const presetDir = path.join(repoRoot, '.claude', 'presets', presetName);
|
|
53
|
+
|
|
54
|
+
logger.debug('preset-loader - loadPreset', 'Loading preset from', { presetDir });
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Load preset.json (metadata)
|
|
58
|
+
const presetJsonPath = path.join(presetDir, 'preset.json');
|
|
59
|
+
const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
|
|
60
|
+
const metadata = JSON.parse(metadataRaw);
|
|
61
|
+
|
|
62
|
+
// Load config.json (overrides)
|
|
63
|
+
const configJsonPath = path.join(presetDir, 'config.json');
|
|
64
|
+
const configRaw = await fs.readFile(configJsonPath, 'utf8');
|
|
65
|
+
const config = JSON.parse(configRaw);
|
|
66
|
+
|
|
67
|
+
// Resolve template paths
|
|
68
|
+
const templates = {};
|
|
69
|
+
for (const [key, templatePath] of Object.entries(metadata.templates)) {
|
|
70
|
+
templates[key] = path.join(presetDir, templatePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
'preset-loader - loadPreset',
|
|
75
|
+
'Preset loaded successfully',
|
|
76
|
+
{
|
|
77
|
+
presetName,
|
|
78
|
+
displayName: metadata.displayName,
|
|
79
|
+
fileExtensions: metadata.fileExtensions,
|
|
80
|
+
templateCount: Object.keys(templates).length
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { metadata, config, templates };
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(
|
|
88
|
+
'preset-loader - loadPreset',
|
|
89
|
+
`Failed to load preset: ${presetName}`,
|
|
90
|
+
error
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
throw new PresetError(`Preset "${presetName}" not found or invalid`, {
|
|
94
|
+
presetName,
|
|
95
|
+
cause: error
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Loads a template file and replaces placeholders
|
|
102
|
+
* @param {string} templatePath - Absolute path to template file
|
|
103
|
+
* @param {Object} metadata - Preset metadata for placeholder replacement
|
|
104
|
+
* @returns {Promise<string>} Template content with placeholders replaced
|
|
105
|
+
*/
|
|
106
|
+
export async function loadTemplate(templatePath, metadata) {
|
|
107
|
+
logger.debug(
|
|
108
|
+
'preset-loader - loadTemplate',
|
|
109
|
+
'Loading template',
|
|
110
|
+
{ templatePath }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
let template = await fs.readFile(templatePath, 'utf8');
|
|
115
|
+
|
|
116
|
+
// Replace placeholders
|
|
117
|
+
template = template.replace(
|
|
118
|
+
/{{TECH_STACK}}/g,
|
|
119
|
+
metadata.techStack.join(', ')
|
|
120
|
+
);
|
|
121
|
+
template = template.replace(
|
|
122
|
+
/{{FOCUS_AREAS}}/g,
|
|
123
|
+
metadata.focusAreas.join(', ')
|
|
124
|
+
);
|
|
125
|
+
template = template.replace(
|
|
126
|
+
/{{FILE_EXTENSIONS}}/g,
|
|
127
|
+
metadata.fileExtensions.join(', ')
|
|
128
|
+
);
|
|
129
|
+
template = template.replace(
|
|
130
|
+
/{{PRESET_NAME}}/g,
|
|
131
|
+
metadata.name
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
logger.debug(
|
|
135
|
+
'preset-loader - loadTemplate',
|
|
136
|
+
'Template loaded and processed',
|
|
137
|
+
{
|
|
138
|
+
templatePath,
|
|
139
|
+
originalLength: template.length
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return template;
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error(
|
|
147
|
+
'preset-loader - loadTemplate',
|
|
148
|
+
`Failed to load template: ${templatePath}`,
|
|
149
|
+
error
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
throw new PresetError(`Template not found: ${templatePath}`, {
|
|
153
|
+
cause: error
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Lists all available presets in .claude/presets/
|
|
160
|
+
* @returns {Promise<Array<Object>>} Array of preset metadata
|
|
161
|
+
* Returns: [{ name, displayName, description }]
|
|
162
|
+
*/
|
|
163
|
+
export async function listPresets() {
|
|
164
|
+
logger.debug('preset-loader - listPresets', 'Listing available presets');
|
|
165
|
+
|
|
166
|
+
const repoRoot = getRepoRoot();
|
|
167
|
+
const presets = [];
|
|
168
|
+
|
|
169
|
+
// Load all presets from .claude/presets/ (installed by claude-hooks install)
|
|
170
|
+
const presetsDir = path.join(repoRoot, '.claude', 'presets');
|
|
171
|
+
try {
|
|
172
|
+
const presetNames = await fs.readdir(presetsDir);
|
|
173
|
+
|
|
174
|
+
for (const name of presetNames) {
|
|
175
|
+
const presetJsonPath = path.join(presetsDir, name, 'preset.json');
|
|
176
|
+
try {
|
|
177
|
+
const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
|
|
178
|
+
const metadata = JSON.parse(metadataRaw);
|
|
179
|
+
|
|
180
|
+
presets.push({
|
|
181
|
+
name: metadata.name,
|
|
182
|
+
displayName: metadata.displayName,
|
|
183
|
+
description: metadata.description
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.debug(
|
|
187
|
+
'preset-loader - listPresets',
|
|
188
|
+
`Skipping invalid preset: ${name}`,
|
|
189
|
+
error
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const errorMsg = formatError('No presets directory found', [
|
|
195
|
+
`Expected location: ${presetsDir}`
|
|
196
|
+
]);
|
|
197
|
+
logger.warning(errorMsg);
|
|
198
|
+
logger.debug(
|
|
199
|
+
'preset-loader - listPresets',
|
|
200
|
+
'Failed to read presets directory',
|
|
201
|
+
error
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.debug(
|
|
206
|
+
'preset-loader - listPresets',
|
|
207
|
+
'Presets listed',
|
|
208
|
+
{ count: presets.length }
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return presets;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { PresetError };
|
|
@@ -38,14 +38,22 @@ class PromptBuilderError extends Error {
|
|
|
38
38
|
* Why absolute path: Ensures templates are found regardless of cwd (cross-platform)
|
|
39
39
|
*
|
|
40
40
|
* @param {string} templateName - Name of template file
|
|
41
|
-
* @param {string} baseDir - Base directory (default: .claude)
|
|
41
|
+
* @param {string} baseDir - Base directory (default: try .claude, fallback to templates)
|
|
42
42
|
* @returns {Promise<string>} Template content
|
|
43
43
|
* @throws {PromptBuilderError} If template not found
|
|
44
44
|
*/
|
|
45
45
|
const loadTemplate = async (templateName, baseDir = '.claude') => {
|
|
46
46
|
// Why: Use repo root for absolute path (works on Windows/PowerShell/Git Bash)
|
|
47
47
|
const repoRoot = getRepoRoot();
|
|
48
|
-
|
|
48
|
+
let templatePath = path.join(repoRoot, baseDir, templateName);
|
|
49
|
+
|
|
50
|
+
// Try .claude first, fallback to templates/ if not found
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(templatePath);
|
|
53
|
+
} catch {
|
|
54
|
+
// Try templates/ directory as fallback
|
|
55
|
+
templatePath = path.join(repoRoot, 'templates', templateName);
|
|
56
|
+
}
|
|
49
57
|
|
|
50
58
|
logger.debug(
|
|
51
59
|
'prompt-builder - loadTemplate',
|
|
@@ -104,6 +112,57 @@ const replaceTemplate = (template, variables) => {
|
|
|
104
112
|
}, template);
|
|
105
113
|
};
|
|
106
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Loads a prompt template and replaces variables
|
|
117
|
+
* Why: High-level interface that combines loading and variable replacement
|
|
118
|
+
*
|
|
119
|
+
* @param {string} templateName - Name of template file
|
|
120
|
+
* @param {Object} variables - Variables to replace in template
|
|
121
|
+
* @param {string} baseDir - Base directory (default: .claude)
|
|
122
|
+
* @returns {Promise<string>} Prompt with replaced variables
|
|
123
|
+
* @throws {PromptBuilderError} If template not found
|
|
124
|
+
*
|
|
125
|
+
* Example:
|
|
126
|
+
* const prompt = await loadPrompt('COMMIT_MESSAGE.md', {
|
|
127
|
+
* FILE_LIST: 'file1.js\nfile2.js',
|
|
128
|
+
* FILE_COUNT: 2,
|
|
129
|
+
* INSERTIONS: 10,
|
|
130
|
+
* DELETIONS: 5
|
|
131
|
+
* });
|
|
132
|
+
*/
|
|
133
|
+
const loadPrompt = async (templateName, variables = {}, baseDir = '.claude') => {
|
|
134
|
+
logger.debug(
|
|
135
|
+
'prompt-builder - loadPrompt',
|
|
136
|
+
'Loading prompt with variables',
|
|
137
|
+
{ templateName, variableCount: Object.keys(variables).length }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Load template
|
|
142
|
+
const template = await loadTemplate(templateName, baseDir);
|
|
143
|
+
|
|
144
|
+
// Replace variables
|
|
145
|
+
const prompt = replaceTemplate(template, variables);
|
|
146
|
+
|
|
147
|
+
logger.debug(
|
|
148
|
+
'prompt-builder - loadPrompt',
|
|
149
|
+
'Prompt loaded successfully',
|
|
150
|
+
{ templateName, promptLength: prompt.length }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return prompt;
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.error(
|
|
157
|
+
'prompt-builder - loadPrompt',
|
|
158
|
+
`Failed to load prompt: ${templateName}`,
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
107
166
|
/**
|
|
108
167
|
* Formats file information for prompt
|
|
109
168
|
* Why: Structures file data in readable format for Claude
|
|
@@ -144,6 +203,7 @@ const formatFileSection = ({ path, diff, content, isNew }) => {
|
|
|
144
203
|
* @param {string} options.guidelinesName - Guidelines filename
|
|
145
204
|
* @param {Array<Object>} options.files - Array of file data objects
|
|
146
205
|
* @param {Object} options.metadata - Additional metadata (repo name, branch, etc.)
|
|
206
|
+
* @param {Object} options.subagentConfig - Subagent configuration (optional)
|
|
147
207
|
* @returns {Promise<string>} Complete analysis prompt
|
|
148
208
|
*
|
|
149
209
|
* File data object structure:
|
|
@@ -158,12 +218,13 @@ const buildAnalysisPrompt = async ({
|
|
|
158
218
|
templateName = 'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
|
|
159
219
|
guidelinesName = 'CLAUDE_PRE_COMMIT_SONAR.md',
|
|
160
220
|
files = [],
|
|
161
|
-
metadata = {}
|
|
221
|
+
metadata = {},
|
|
222
|
+
subagentConfig = null
|
|
162
223
|
} = {}) => {
|
|
163
224
|
logger.debug(
|
|
164
225
|
'prompt-builder - buildAnalysisPrompt',
|
|
165
226
|
'Building analysis prompt',
|
|
166
|
-
{ templateName, guidelinesName, fileCount: files.length }
|
|
227
|
+
{ templateName, guidelinesName, fileCount: files.length, subagentsEnabled: subagentConfig?.enabled }
|
|
167
228
|
);
|
|
168
229
|
|
|
169
230
|
try {
|
|
@@ -176,6 +237,22 @@ const buildAnalysisPrompt = async ({
|
|
|
176
237
|
// Start with template
|
|
177
238
|
let prompt = template;
|
|
178
239
|
|
|
240
|
+
// Add subagent instruction if enabled and 3+ files
|
|
241
|
+
if (subagentConfig?.enabled && files.length >= 3) {
|
|
242
|
+
try {
|
|
243
|
+
const subagentInstruction = await loadTemplate('SUBAGENT_INSTRUCTION.md');
|
|
244
|
+
const subagentVariables = {
|
|
245
|
+
BATCH_SIZE: subagentConfig.batchSize || 3,
|
|
246
|
+
MODEL: subagentConfig.model || 'haiku'
|
|
247
|
+
};
|
|
248
|
+
prompt += '\n\n' + replaceTemplate(subagentInstruction, subagentVariables) + '\n';
|
|
249
|
+
|
|
250
|
+
logger.info(`🚀 Batch optimization enabled: ${files.length} files, ${subagentVariables.BATCH_SIZE} per batch, ${subagentVariables.MODEL} model`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.warning('Subagent instruction template not found, proceeding without parallel analysis');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
179
256
|
// Add guidelines section
|
|
180
257
|
prompt += '\n\n=== EVALUATION GUIDELINES ===\n';
|
|
181
258
|
prompt += guidelines;
|
|
@@ -212,72 +289,11 @@ const buildAnalysisPrompt = async ({
|
|
|
212
289
|
}
|
|
213
290
|
};
|
|
214
291
|
|
|
215
|
-
/**
|
|
216
|
-
* Builds commit message generation prompt
|
|
217
|
-
* Why: Used by prepare-commit-msg hook
|
|
218
|
-
*
|
|
219
|
-
* @param {Object} options - Build options
|
|
220
|
-
* @param {Array<Object>} options.files - Array of changed files with diffs
|
|
221
|
-
* @param {Object} options.stats - Staging statistics
|
|
222
|
-
* @returns {Promise<string>} Complete commit message prompt
|
|
223
|
-
*
|
|
224
|
-
* Stats object structure:
|
|
225
|
-
* {
|
|
226
|
-
* totalFiles: number,
|
|
227
|
-
* addedFiles: number,
|
|
228
|
-
* modifiedFiles: number,
|
|
229
|
-
* deletedFiles: number,
|
|
230
|
-
* insertions: number,
|
|
231
|
-
* deletions: number
|
|
232
|
-
* }
|
|
233
|
-
*/
|
|
234
|
-
const buildCommitMessagePrompt = async ({ files = [], stats = {} } = {}) => {
|
|
235
|
-
logger.debug(
|
|
236
|
-
'prompt-builder - buildCommitMessagePrompt',
|
|
237
|
-
'Building commit message prompt',
|
|
238
|
-
{ fileCount: files.length, stats }
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
let prompt = `Generate a conventional commit message for the following changes.\n\n`;
|
|
242
|
-
|
|
243
|
-
// Add statistics
|
|
244
|
-
prompt += `=== STATISTICS ===\n`;
|
|
245
|
-
prompt += `Files changed: ${stats.totalFiles || files.length}\n`;
|
|
246
|
-
prompt += `Added: ${stats.addedFiles || 0}, Modified: ${stats.modifiedFiles || 0}, Deleted: ${stats.deletedFiles || 0}\n`;
|
|
247
|
-
prompt += `Insertions: ${stats.insertions || 0}, Deletions: ${stats.deletions || 0}\n\n`;
|
|
248
|
-
|
|
249
|
-
// Add file changes
|
|
250
|
-
prompt += `=== CHANGED FILES ===\n`;
|
|
251
|
-
files.forEach(({ path, diff }) => {
|
|
252
|
-
prompt += `\n--- ${path} ---\n`;
|
|
253
|
-
if (diff) {
|
|
254
|
-
prompt += `${diff}\n`;
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Add instructions
|
|
259
|
-
prompt += `\n\n=== INSTRUCTIONS ===\n`;
|
|
260
|
-
prompt += `Generate a concise commit message following Conventional Commits format:\n`;
|
|
261
|
-
prompt += `- type: feat, fix, docs, style, refactor, test, chore\n`;
|
|
262
|
-
prompt += `- Format: "type: description" or "type(scope): description"\n`;
|
|
263
|
-
prompt += `- Description: Clear, imperative mood, no period at end\n`;
|
|
264
|
-
prompt += `- Optional body: Detailed explanation if needed\n\n`;
|
|
265
|
-
prompt += `Respond with ONLY the commit message, no explanations.\n`;
|
|
266
|
-
|
|
267
|
-
logger.debug(
|
|
268
|
-
'prompt-builder - buildCommitMessagePrompt',
|
|
269
|
-
'Commit message prompt built',
|
|
270
|
-
{ promptLength: prompt.length }
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
return prompt;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
292
|
export {
|
|
277
293
|
PromptBuilderError,
|
|
278
294
|
loadTemplate,
|
|
279
295
|
replaceTemplate,
|
|
296
|
+
loadPrompt,
|
|
280
297
|
formatFileSection,
|
|
281
|
-
buildAnalysisPrompt
|
|
282
|
-
buildCommitMessagePrompt
|
|
298
|
+
buildAnalysisPrompt
|
|
283
299
|
};
|
|
@@ -179,14 +179,20 @@ ${content}
|
|
|
179
179
|
const generateResolutionPrompt = async (
|
|
180
180
|
analysisResult,
|
|
181
181
|
{
|
|
182
|
-
outputPath =
|
|
182
|
+
outputPath = null,
|
|
183
183
|
templatePath = '.claude/CLAUDE_RESOLUTION_PROMPT.md',
|
|
184
184
|
fileCount = 0
|
|
185
185
|
} = {}
|
|
186
186
|
) => {
|
|
187
187
|
// Why: Use repo root for absolute paths (works on Windows/PowerShell/Git Bash)
|
|
188
188
|
const repoRoot = getRepoRoot();
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
// Load config to get default output path
|
|
191
|
+
const { getConfig } = await import('../config.js');
|
|
192
|
+
const config = await getConfig();
|
|
193
|
+
const finalOutputPath = outputPath || config.output.resolutionFile;
|
|
194
|
+
|
|
195
|
+
const absoluteOutputPath = path.join(repoRoot, finalOutputPath);
|
|
190
196
|
const absoluteTemplatePath = path.join(repoRoot, templatePath);
|
|
191
197
|
|
|
192
198
|
logger.debug(
|
|
@@ -226,6 +232,10 @@ const generateResolutionPrompt = async (
|
|
|
226
232
|
.replace(/{{BLOCKING_ISSUES}}/g, issuesFormatted)
|
|
227
233
|
.replace(/{{FILE_CONTENTS}}/g, fileContentsFormatted);
|
|
228
234
|
|
|
235
|
+
// Ensure output directory exists
|
|
236
|
+
const outputDir = path.dirname(absoluteOutputPath);
|
|
237
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
238
|
+
|
|
229
239
|
// Write resolution prompt file
|
|
230
240
|
await fs.writeFile(absoluteOutputPath, prompt, 'utf8');
|
|
231
241
|
|