@paulduvall/claude-dev-toolkit 0.0.1-alpha.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.
@@ -0,0 +1,358 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Validates security hooks and their metadata
6
+ * Extracted from hook-installer.js for better separation of concerns
7
+ */
8
+ class HookValidator {
9
+ constructor() {
10
+ this.validationRules = {
11
+ shebangs: ['#!/bin/bash', '#!/bin/sh', '#!/usr/bin/env bash', '#!/usr/bin/env sh'],
12
+ securityKeywords: ['credential', 'security', 'validate', 'check', 'prevent'],
13
+ requiredMetadata: ['Description', 'Purpose', 'Trigger'],
14
+ maxFileSize: 100 * 1024 // 100KB
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Validate a hook file
20
+ * @param {string} hookPath - Path to hook file
21
+ * @returns {Object} Validation result with details
22
+ */
23
+ validateHook(hookPath) {
24
+ const result = {
25
+ valid: false,
26
+ errors: [],
27
+ warnings: [],
28
+ metadata: {}
29
+ };
30
+
31
+ try {
32
+ // Check if file exists
33
+ if (!fs.existsSync(hookPath)) {
34
+ result.errors.push('Hook file not found');
35
+ return result;
36
+ }
37
+
38
+ // Check file size
39
+ const stats = fs.statSync(hookPath);
40
+ if (stats.size > this.validationRules.maxFileSize) {
41
+ result.warnings.push(`File size (${stats.size} bytes) exceeds recommended maximum (${this.validationRules.maxFileSize} bytes)`);
42
+ }
43
+
44
+ // Read and validate content
45
+ const content = fs.readFileSync(hookPath, 'utf8');
46
+
47
+ // Validate shebang
48
+ const shebangValid = this._validateShebang(content);
49
+ if (!shebangValid.valid) {
50
+ result.errors.push(shebangValid.error);
51
+ }
52
+
53
+ // Validate security content
54
+ const securityValid = this._validateSecurityContent(content);
55
+ if (!securityValid.valid) {
56
+ result.errors.push(securityValid.error);
57
+ }
58
+
59
+ // Extract and validate metadata
60
+ result.metadata = this._extractMetadata(content);
61
+ const metadataValid = this._validateMetadata(result.metadata);
62
+ if (!metadataValid.valid) {
63
+ result.warnings.push(...metadataValid.warnings);
64
+ }
65
+
66
+ // Check for executable permissions
67
+ const executableValid = this._validateExecutablePermissions(hookPath);
68
+ if (!executableValid.valid) {
69
+ result.warnings.push(executableValid.warning);
70
+ }
71
+
72
+ // Overall validation
73
+ result.valid = result.errors.length === 0;
74
+
75
+ return result;
76
+
77
+ } catch (error) {
78
+ result.errors.push(`Validation failed: ${error.message}`);
79
+ return result;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Validate multiple hooks in a directory
85
+ * @param {string} hooksDir - Directory containing hooks
86
+ * @returns {Object} Validation results for all hooks
87
+ */
88
+ validateHooksDirectory(hooksDir) {
89
+ const result = {
90
+ valid: true,
91
+ totalHooks: 0,
92
+ validHooks: 0,
93
+ invalidHooks: 0,
94
+ results: {}
95
+ };
96
+
97
+ try {
98
+ if (!fs.existsSync(hooksDir)) {
99
+ result.valid = false;
100
+ result.error = 'Hooks directory not found';
101
+ return result;
102
+ }
103
+
104
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
105
+ result.totalHooks = hookFiles.length;
106
+
107
+ for (const hookFile of hookFiles) {
108
+ const hookPath = path.join(hooksDir, hookFile);
109
+ const hookName = path.basename(hookFile, '.sh');
110
+ const validation = this.validateHook(hookPath);
111
+
112
+ result.results[hookName] = validation;
113
+
114
+ if (validation.valid) {
115
+ result.validHooks++;
116
+ } else {
117
+ result.invalidHooks++;
118
+ }
119
+ }
120
+
121
+ result.valid = result.invalidHooks === 0;
122
+
123
+ return result;
124
+
125
+ } catch (error) {
126
+ result.valid = false;
127
+ result.error = `Directory validation failed: ${error.message}`;
128
+ return result;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get validation summary for a set of hooks
134
+ * @param {Array<string>} hookPaths - Array of hook file paths
135
+ * @returns {Object} Validation summary
136
+ */
137
+ getValidationSummary(hookPaths) {
138
+ const summary = {
139
+ total: hookPaths.length,
140
+ valid: 0,
141
+ invalid: 0,
142
+ warnings: 0,
143
+ commonIssues: {},
144
+ recommendations: []
145
+ };
146
+
147
+ const allResults = hookPaths.map(hookPath => {
148
+ const hookName = path.basename(hookPath, '.sh');
149
+ return {
150
+ name: hookName,
151
+ result: this.validateHook(hookPath)
152
+ };
153
+ });
154
+
155
+ // Aggregate results
156
+ for (const { result } of allResults) {
157
+ if (result.valid) {
158
+ summary.valid++;
159
+ } else {
160
+ summary.invalid++;
161
+ }
162
+
163
+ summary.warnings += result.warnings.length;
164
+
165
+ // Track common issues
166
+ for (const error of result.errors) {
167
+ summary.commonIssues[error] = (summary.commonIssues[error] || 0) + 1;
168
+ }
169
+ }
170
+
171
+ // Generate recommendations
172
+ summary.recommendations = this._generateRecommendations(summary.commonIssues);
173
+
174
+ return summary;
175
+ }
176
+
177
+ /**
178
+ * Validate hook shebang (private method)
179
+ * @param {string} content - Hook file content
180
+ * @returns {Object} Validation result
181
+ * @private
182
+ */
183
+ _validateShebang(content) {
184
+ const lines = content.split('\n');
185
+ const firstLine = lines[0];
186
+
187
+ const hasValidShebang = this.validationRules.shebangs.some(shebang =>
188
+ firstLine.startsWith(shebang)
189
+ );
190
+
191
+ return {
192
+ valid: hasValidShebang,
193
+ error: hasValidShebang ? null : `Invalid or missing shebang. Expected one of: ${this.validationRules.shebangs.join(', ')}`
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Validate security content (private method)
199
+ * @param {string} content - Hook file content
200
+ * @returns {Object} Validation result
201
+ * @private
202
+ */
203
+ _validateSecurityContent(content) {
204
+ const lowerContent = content.toLowerCase();
205
+ const hasSecurityContent = this.validationRules.securityKeywords.some(keyword =>
206
+ lowerContent.includes(keyword)
207
+ );
208
+
209
+ return {
210
+ valid: hasSecurityContent,
211
+ error: hasSecurityContent ? null : `Hook does not appear to contain security-related functionality. Expected keywords: ${this.validationRules.securityKeywords.join(', ')}`
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Extract metadata from hook content (private method)
217
+ * @param {string} content - Hook file content
218
+ * @returns {Object} Extracted metadata
219
+ * @private
220
+ */
221
+ _extractMetadata(content) {
222
+ const metadata = {
223
+ trigger: 'PreToolUse',
224
+ blocking: true,
225
+ tools: [],
226
+ author: 'Claude Dev Toolkit',
227
+ version: '1.0.0',
228
+ category: 'security',
229
+ description: null,
230
+ purpose: null
231
+ };
232
+
233
+ const lines = content.split('\n').slice(0, 20); // Check first 20 lines
234
+
235
+ for (const line of lines) {
236
+ const cleanLine = line.replace(/^#\s*/, '').trim();
237
+
238
+ if (cleanLine.startsWith('Trigger:')) {
239
+ metadata.trigger = cleanLine.replace('Trigger:', '').trim();
240
+ } else if (cleanLine.startsWith('Blocking:')) {
241
+ metadata.blocking = cleanLine.replace('Blocking:', '').trim().toLowerCase() === 'yes';
242
+ } else if (cleanLine.startsWith('Tools:')) {
243
+ const toolsStr = cleanLine.replace('Tools:', '').trim();
244
+ metadata.tools = toolsStr.split(',').map(t => t.trim()).filter(Boolean);
245
+ } else if (cleanLine.startsWith('Author:')) {
246
+ metadata.author = cleanLine.replace('Author:', '').trim();
247
+ } else if (cleanLine.startsWith('Version:')) {
248
+ metadata.version = cleanLine.replace('Version:', '').trim();
249
+ } else if (cleanLine.startsWith('Category:')) {
250
+ metadata.category = cleanLine.replace('Category:', '').trim();
251
+ } else if (cleanLine.startsWith('Description:')) {
252
+ metadata.description = cleanLine.replace('Description:', '').trim();
253
+ } else if (cleanLine.startsWith('Purpose:')) {
254
+ metadata.purpose = cleanLine.replace('Purpose:', '').trim();
255
+ }
256
+ }
257
+
258
+ return metadata;
259
+ }
260
+
261
+ /**
262
+ * Validate hook metadata (private method)
263
+ * @param {Object} metadata - Extracted metadata
264
+ * @returns {Object} Validation result
265
+ * @private
266
+ */
267
+ _validateMetadata(metadata) {
268
+ const result = {
269
+ valid: true,
270
+ warnings: []
271
+ };
272
+
273
+ // Check for required metadata
274
+ if (!metadata.description && !metadata.purpose) {
275
+ result.warnings.push('Hook missing description or purpose metadata');
276
+ }
277
+
278
+ if (!metadata.version || metadata.version === '1.0.0') {
279
+ result.warnings.push('Hook should specify a version number');
280
+ }
281
+
282
+ if (!metadata.author || metadata.author === 'Claude Dev Toolkit') {
283
+ result.warnings.push('Hook should specify an author');
284
+ }
285
+
286
+ if (metadata.tools.length === 0) {
287
+ result.warnings.push('Hook does not specify which tools it applies to');
288
+ }
289
+
290
+ return result;
291
+ }
292
+
293
+ /**
294
+ * Validate executable permissions (private method)
295
+ * @param {string} hookPath - Path to hook file
296
+ * @returns {Object} Validation result
297
+ * @private
298
+ */
299
+ _validateExecutablePermissions(hookPath) {
300
+ try {
301
+ const stats = fs.statSync(hookPath);
302
+ const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
303
+
304
+ return {
305
+ valid: isExecutable,
306
+ warning: isExecutable ? null : 'Hook file is not executable (should have execute permissions)'
307
+ };
308
+ } catch (error) {
309
+ return {
310
+ valid: false,
311
+ warning: `Cannot check executable permissions: ${error.message}`
312
+ };
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Generate recommendations based on common issues (private method)
318
+ * @param {Object} commonIssues - Map of issues to their frequency
319
+ * @returns {Array<string>} List of recommendations
320
+ * @private
321
+ */
322
+ _generateRecommendations(commonIssues) {
323
+ const recommendations = [];
324
+ const sortedIssues = Object.entries(commonIssues).sort((a, b) => b[1] - a[1]);
325
+
326
+ for (const [issue, count] of sortedIssues.slice(0, 5)) { // Top 5 issues
327
+ if (issue.includes('shebang')) {
328
+ recommendations.push('Add proper shebang line (#!/bin/bash) to hook files');
329
+ } else if (issue.includes('security')) {
330
+ recommendations.push('Include security-related keywords and functionality in hooks');
331
+ } else if (issue.includes('executable')) {
332
+ recommendations.push('Make hook files executable with chmod +x');
333
+ } else if (issue.includes('metadata')) {
334
+ recommendations.push('Add descriptive metadata comments to hooks');
335
+ }
336
+ }
337
+
338
+ return recommendations;
339
+ }
340
+
341
+ /**
342
+ * Get validation rules
343
+ * @returns {Object} Current validation rules
344
+ */
345
+ getValidationRules() {
346
+ return { ...this.validationRules };
347
+ }
348
+
349
+ /**
350
+ * Update validation rules
351
+ * @param {Object} newRules - New rules to merge with existing ones
352
+ */
353
+ updateValidationRules(newRules) {
354
+ this.validationRules = { ...this.validationRules, ...newRules };
355
+ }
356
+ }
357
+
358
+ module.exports = HookValidator;