@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,187 @@
1
+ // Security Hook Installer for Claude Dev Toolkit
2
+ // Implements REQ-018: Security Hook Installation
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Import modular components for better separation of concerns
7
+ const HookInstaller = require('./hook-installer-core');
8
+ const HookValidator = require('./hook-validator');
9
+ const HookMetadataService = require('./hook-metadata-service');
10
+
11
+ // Create singleton instances
12
+ const hookInstaller = new HookInstaller();
13
+ const hookValidator = new HookValidator();
14
+ const hookMetadataService = new HookMetadataService();
15
+
16
+ /**
17
+ * Install security hooks to the specified directory
18
+ * Implements REQ-018: Security Hook Installation
19
+ *
20
+ * @param {string} targetHooksDir - Directory to install hooks to
21
+ * @param {Array|string} hookNames - Array of hook names or single hook name to install
22
+ * @param {Object} options - Installation options
23
+ * @param {boolean} options.force - Force overwrite existing hooks
24
+ * @param {boolean} options.validate - Validate hooks before installation
25
+ * @param {boolean} options.backup - Create backup of existing hooks
26
+ * @returns {Object} - Installation result with details
27
+ */
28
+ function installSecurityHooks(targetHooksDir, hookNames, options = {}) {
29
+ return hookInstaller.installSecurityHooks(targetHooksDir, hookNames, options);
30
+ }
31
+
32
+ // Legacy functions maintained for backward compatibility
33
+
34
+ /**
35
+ * Get the source hooks directory path
36
+ * @returns {string} - Path to source hooks directory
37
+ */
38
+ function getSourceHooksDirectory() {
39
+ return hookInstaller.getSourceHooksDirectory();
40
+ }
41
+
42
+ /**
43
+ * Get available security hooks with caching for performance
44
+ * @param {boolean} forceRefresh - Force refresh of the cache
45
+ * @returns {Array} - Array of available hook information
46
+ */
47
+ function getAvailableHooks(forceRefresh = false) {
48
+ return hookMetadataService.getAvailableHooks(forceRefresh);
49
+ }
50
+
51
+ // Legacy functions - delegated to metadata service for backward compatibility
52
+
53
+ /**
54
+ * Get detailed metadata from hook file
55
+ * @param {string} hookPath - Path to hook file
56
+ * @returns {Object} - Hook metadata
57
+ */
58
+ function getHookMetadata(hookPath) {
59
+ const hooks = hookMetadataService.getAvailableHooks();
60
+ const hookName = path.basename(hookPath, '.sh');
61
+ const hook = hooks.find(h => h.name === hookName);
62
+ return hook ? hook.metadata : {};
63
+ }
64
+
65
+ /**
66
+ * Get file size in bytes
67
+ * @param {string} filePath - Path to file
68
+ * @returns {number} - File size in bytes
69
+ */
70
+ function getFileSize(filePath) {
71
+ const hooks = hookMetadataService.getAvailableHooks();
72
+ const hookName = path.basename(filePath, '.sh');
73
+ const hook = hooks.find(h => h.name === hookName);
74
+ return hook ? hook.size : 0;
75
+ }
76
+
77
+ /**
78
+ * Get description from hook file
79
+ * @param {string} hookPath - Path to hook file
80
+ * @returns {string} - Hook description
81
+ */
82
+ function getHookDescription(hookPath) {
83
+ const hooks = hookMetadataService.getAvailableHooks();
84
+ const hookName = path.basename(hookPath, '.sh');
85
+ const hook = hooks.find(h => h.name === hookName);
86
+ return hook ? hook.description : 'Security hook';
87
+ }
88
+
89
+ /**
90
+ * Validate a hook file
91
+ * @param {string} hookPath - Path to hook file
92
+ * @returns {boolean} - True if valid, false otherwise
93
+ */
94
+ function validateHook(hookPath) {
95
+ const result = hookValidator.validateHook(hookPath);
96
+ return result.valid;
97
+ }
98
+
99
+ /**
100
+ * Get installation log
101
+ * @param {boolean} clear - Whether to clear the log after retrieving
102
+ * @returns {Array} - Installation log entries
103
+ */
104
+ function getInstallationLog(clear = false) {
105
+ return hookInstaller.getInstallationLog(clear);
106
+ }
107
+
108
+ /**
109
+ * Remove installed security hooks
110
+ * @param {string} targetHooksDir - Directory containing installed hooks
111
+ * @param {Array|string} hookNames - Array of hook names or single hook name to remove
112
+ * @returns {Object} - Removal result with details
113
+ */
114
+ function removeSecurityHooks(targetHooksDir, hookNames) {
115
+ return hookInstaller.removeSecurityHooks(targetHooksDir, hookNames);
116
+ }
117
+
118
+ /**
119
+ * Get hook installation summary
120
+ * @returns {Object} - Summary of hook installations and system status
121
+ */
122
+ function getHookInstallationSummary() {
123
+ const installerSummary = hookInstaller.getHookInstallationSummary();
124
+ const metadataStats = hookMetadataService.getHookStats();
125
+
126
+ return {
127
+ ...installerSummary,
128
+ availableHooks: metadataStats.total,
129
+ validHooks: metadataStats.valid
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Clear hook metadata cache (useful for testing or after hook updates)
135
+ */
136
+ function clearHookCache() {
137
+ hookMetadataService.clearCache();
138
+ }
139
+
140
+ /**
141
+ * Backward-compatible wrapper for installSecurityHooks
142
+ * Returns boolean for simple cases, detailed object for complex cases
143
+ */
144
+ function installSecurityHooksCompat(targetHooksDir, hookNames, options = {}) {
145
+ const result = installSecurityHooks(targetHooksDir, hookNames, options);
146
+
147
+ // For backward compatibility, return boolean if no options specified
148
+ if (!options || Object.keys(options).length === 0) {
149
+ return result.success;
150
+ }
151
+
152
+ // Return full result object for advanced usage
153
+ return result;
154
+ }
155
+
156
+ module.exports = {
157
+ // Core functionality (REQ-018 implementation)
158
+ installSecurityHooks: installSecurityHooksCompat,
159
+ removeSecurityHooks,
160
+ getAvailableHooks,
161
+ validateHook,
162
+
163
+ // Logging and monitoring
164
+ getInstallationLog,
165
+ getHookInstallationSummary,
166
+
167
+ // Utility functions
168
+ clearHookCache,
169
+
170
+ // Internal functions exposed for testing (legacy support)
171
+ getSourceHooksDirectory,
172
+ getHookMetadata,
173
+ getFileSize,
174
+
175
+ // Advanced API (returns detailed objects)
176
+ installSecurityHooksDetailed: installSecurityHooks,
177
+
178
+ // Access to modular components for advanced usage
179
+ HookInstaller,
180
+ HookValidator,
181
+ HookMetadataService,
182
+
183
+ // Component instances
184
+ installer: hookInstaller,
185
+ validator: hookValidator,
186
+ metadataService: hookMetadataService
187
+ };
@@ -0,0 +1,352 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Manages hook metadata and discovery
6
+ * Extracted from hook-installer.js for better separation of concerns
7
+ */
8
+ class HookMetadataService {
9
+ constructor() {
10
+ this.metadataCache = null;
11
+ this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
12
+ this.lastCacheUpdate = null;
13
+ }
14
+
15
+ /**
16
+ * Get available security hooks with caching for performance
17
+ * @param {boolean} forceRefresh - Force refresh of the cache
18
+ * @returns {Array} Array of available hook information
19
+ */
20
+ getAvailableHooks(forceRefresh = false) {
21
+ try {
22
+ // Return cached data if available and not forcing refresh
23
+ if (this._isCacheValid() && !forceRefresh) {
24
+ return this.metadataCache;
25
+ }
26
+
27
+ const sourceHooksDir = this._getSourceHooksDirectory();
28
+
29
+ if (!fs.existsSync(sourceHooksDir)) {
30
+ this.metadataCache = [];
31
+ this.lastCacheUpdate = Date.now();
32
+ return this.metadataCache;
33
+ }
34
+
35
+ const hookFiles = fs.readdirSync(sourceHooksDir).filter(f => f.endsWith('.sh'));
36
+
37
+ this.metadataCache = hookFiles.map(file => {
38
+ const name = path.basename(file, '.sh');
39
+ const hookPath = path.join(sourceHooksDir, file);
40
+
41
+ return {
42
+ name,
43
+ filename: file,
44
+ path: hookPath,
45
+ description: this._getHookDescription(hookPath),
46
+ metadata: this._getHookMetadata(hookPath),
47
+ valid: this._isHookValid(hookPath),
48
+ size: this._getFileSize(hookPath),
49
+ lastModified: this._getLastModified(hookPath),
50
+ checksum: this._calculateChecksum(hookPath)
51
+ };
52
+ }).sort((a, b) => a.name.localeCompare(b.name));
53
+
54
+ this.lastCacheUpdate = Date.now();
55
+ return this.metadataCache;
56
+ } catch (error) {
57
+ this.metadataCache = [];
58
+ this.lastCacheUpdate = Date.now();
59
+ return this.metadataCache;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get metadata for a specific hook
65
+ * @param {string} hookName - Name of the hook
66
+ * @returns {Object|null} Hook metadata or null if not found
67
+ */
68
+ getHookMetadata(hookName) {
69
+ const hooks = this.getAvailableHooks();
70
+ return hooks.find(hook => hook.name === hookName) || null;
71
+ }
72
+
73
+ /**
74
+ * Search hooks by criteria
75
+ * @param {Object} criteria - Search criteria
76
+ * @param {string} criteria.name - Hook name pattern
77
+ * @param {string} criteria.category - Hook category
78
+ * @param {boolean} criteria.valid - Only valid hooks
79
+ * @param {Array<string>} criteria.tools - Required tools
80
+ * @returns {Array} Matching hooks
81
+ */
82
+ searchHooks(criteria = {}) {
83
+ let hooks = this.getAvailableHooks();
84
+
85
+ if (criteria.name) {
86
+ const namePattern = new RegExp(criteria.name, 'i');
87
+ hooks = hooks.filter(hook => namePattern.test(hook.name));
88
+ }
89
+
90
+ if (criteria.category) {
91
+ hooks = hooks.filter(hook =>
92
+ hook.metadata.category === criteria.category
93
+ );
94
+ }
95
+
96
+ if (criteria.valid !== undefined) {
97
+ hooks = hooks.filter(hook => hook.valid === criteria.valid);
98
+ }
99
+
100
+ if (criteria.tools && criteria.tools.length > 0) {
101
+ hooks = hooks.filter(hook =>
102
+ criteria.tools.some(tool => hook.metadata.tools.includes(tool))
103
+ );
104
+ }
105
+
106
+ return hooks;
107
+ }
108
+
109
+ /**
110
+ * Get hook categories with counts
111
+ * @returns {Object} Categories mapped to hook counts
112
+ */
113
+ getHookCategories() {
114
+ const hooks = this.getAvailableHooks();
115
+ const categories = {};
116
+
117
+ hooks.forEach(hook => {
118
+ const category = hook.metadata.category || 'uncategorized';
119
+ categories[category] = (categories[category] || 0) + 1;
120
+ });
121
+
122
+ return categories;
123
+ }
124
+
125
+ /**
126
+ * Get hooks statistics
127
+ * @returns {Object} Statistics about available hooks
128
+ */
129
+ getHookStats() {
130
+ const hooks = this.getAvailableHooks();
131
+ const validHooks = hooks.filter(hook => hook.valid);
132
+ const categories = this.getHookCategories();
133
+
134
+ return {
135
+ total: hooks.length,
136
+ valid: validHooks.length,
137
+ invalid: hooks.length - validHooks.length,
138
+ categories: Object.keys(categories).length,
139
+ categoryBreakdown: categories,
140
+ averageSize: hooks.length > 0 ?
141
+ Math.round(hooks.reduce((sum, hook) => sum + hook.size, 0) / hooks.length) : 0,
142
+ lastUpdated: this.lastCacheUpdate,
143
+ cacheStatus: this._isCacheValid() ? 'valid' : 'expired'
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Validate hook file content
149
+ * @param {string} hookPath - Path to hook file
150
+ * @returns {boolean} True if valid, false otherwise
151
+ */
152
+ validateHook(hookPath) {
153
+ return this._isHookValid(hookPath);
154
+ }
155
+
156
+ /**
157
+ * Clear metadata cache
158
+ */
159
+ clearCache() {
160
+ this.metadataCache = null;
161
+ this.lastCacheUpdate = null;
162
+ }
163
+
164
+ /**
165
+ * Export hook metadata to JSON
166
+ * @returns {string} JSON representation of all hook metadata
167
+ */
168
+ exportMetadata() {
169
+ const metadata = {
170
+ generated: new Date().toISOString(),
171
+ stats: this.getHookStats(),
172
+ hooks: this.getAvailableHooks()
173
+ };
174
+
175
+ return JSON.stringify(metadata, null, 2);
176
+ }
177
+
178
+ /**
179
+ * Check if cache is valid (private method)
180
+ * @returns {boolean} True if cache is valid
181
+ * @private
182
+ */
183
+ _isCacheValid() {
184
+ if (!this.metadataCache || !this.lastCacheUpdate) {
185
+ return false;
186
+ }
187
+
188
+ return (Date.now() - this.lastCacheUpdate) < this.cacheTimeout;
189
+ }
190
+
191
+ /**
192
+ * Get source hooks directory (private method)
193
+ * @returns {string} Path to source hooks directory
194
+ * @private
195
+ */
196
+ _getSourceHooksDirectory() {
197
+ return path.join(__dirname, '../hooks');
198
+ }
199
+
200
+ /**
201
+ * Get detailed metadata from hook file (private method)
202
+ * @param {string} hookPath - Path to hook file
203
+ * @returns {Object} Hook metadata
204
+ * @private
205
+ */
206
+ _getHookMetadata(hookPath) {
207
+ const metadata = {
208
+ trigger: 'PreToolUse',
209
+ blocking: true,
210
+ tools: [],
211
+ author: 'Claude Dev Toolkit',
212
+ version: '1.0.0',
213
+ category: 'security'
214
+ };
215
+
216
+ try {
217
+ const content = fs.readFileSync(hookPath, 'utf8');
218
+ const lines = content.split('\n').slice(0, 20); // Check first 20 lines
219
+
220
+ for (const line of lines) {
221
+ const cleanLine = line.replace(/^#\s*/, '').trim();
222
+
223
+ if (cleanLine.startsWith('Trigger:')) {
224
+ metadata.trigger = cleanLine.replace('Trigger:', '').trim();
225
+ } else if (cleanLine.startsWith('Blocking:')) {
226
+ metadata.blocking = cleanLine.replace('Blocking:', '').trim().toLowerCase() === 'yes';
227
+ } else if (cleanLine.startsWith('Tools:')) {
228
+ const toolsStr = cleanLine.replace('Tools:', '').trim();
229
+ metadata.tools = toolsStr.split(',').map(t => t.trim()).filter(Boolean);
230
+ } else if (cleanLine.startsWith('Author:')) {
231
+ metadata.author = cleanLine.replace('Author:', '').trim();
232
+ } else if (cleanLine.startsWith('Version:')) {
233
+ metadata.version = cleanLine.replace('Version:', '').trim();
234
+ } else if (cleanLine.startsWith('Category:')) {
235
+ metadata.category = cleanLine.replace('Category:', '').trim();
236
+ }
237
+ }
238
+
239
+ return metadata;
240
+ } catch (error) {
241
+ return metadata;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get description from hook file (private method)
247
+ * @param {string} hookPath - Path to hook file
248
+ * @returns {string} Hook description
249
+ * @private
250
+ */
251
+ _getHookDescription(hookPath) {
252
+ try {
253
+ const content = fs.readFileSync(hookPath, 'utf8');
254
+ const lines = content.split('\n');
255
+
256
+ // Look for description comment in first few lines
257
+ for (const line of lines.slice(0, 10)) {
258
+ if (line.includes('Description:') || line.includes('Purpose:')) {
259
+ return line.replace(/^#\s*/, '').replace(/^Description:\s*/i, '').replace(/^Purpose:\s*/i, '');
260
+ }
261
+ }
262
+
263
+ // Default description based on hook name
264
+ const name = path.basename(hookPath, '.sh');
265
+ return `${name.replace(/-/g, ' ')} security hook`;
266
+ } catch (error) {
267
+ return 'Security hook';
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Get file size in bytes (private method)
273
+ * @param {string} filePath - Path to file
274
+ * @returns {number} File size in bytes
275
+ * @private
276
+ */
277
+ _getFileSize(filePath) {
278
+ try {
279
+ const stats = fs.statSync(filePath);
280
+ return stats.size;
281
+ } catch (error) {
282
+ return 0;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Get file last modified time (private method)
288
+ * @param {string} filePath - Path to file
289
+ * @returns {Date|null} Last modified time or null
290
+ * @private
291
+ */
292
+ _getLastModified(filePath) {
293
+ try {
294
+ const stats = fs.statSync(filePath);
295
+ return stats.mtime;
296
+ } catch (error) {
297
+ return null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Calculate file checksum (private method)
303
+ * @param {string} filePath - Path to file
304
+ * @returns {string} MD5 checksum or empty string
305
+ * @private
306
+ */
307
+ _calculateChecksum(filePath) {
308
+ try {
309
+ const crypto = require('crypto');
310
+ const content = fs.readFileSync(filePath);
311
+ return crypto.createHash('md5').update(content).digest('hex');
312
+ } catch (error) {
313
+ return '';
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Check if hook is valid (private method)
319
+ * @param {string} hookPath - Path to hook file
320
+ * @returns {boolean} True if valid, false otherwise
321
+ * @private
322
+ */
323
+ _isHookValid(hookPath) {
324
+ try {
325
+ if (!fs.existsSync(hookPath)) {
326
+ return false;
327
+ }
328
+
329
+ const content = fs.readFileSync(hookPath, 'utf8');
330
+
331
+ // Basic validation: should have shebang and be executable
332
+ const validShebangs = ['#!/bin/bash', '#!/bin/sh', '#!/usr/bin/env bash', '#!/usr/bin/env sh'];
333
+ const hasValidShebang = validShebangs.some(shebang => content.startsWith(shebang));
334
+
335
+ if (!hasValidShebang) {
336
+ return false;
337
+ }
338
+
339
+ // Should contain some defensive security patterns
340
+ const securityKeywords = ['credential', 'security', 'validate', 'check', 'prevent'];
341
+ const hasSecurityContent = securityKeywords.some(keyword =>
342
+ content.toLowerCase().includes(keyword)
343
+ );
344
+
345
+ return hasSecurityContent;
346
+ } catch (error) {
347
+ return false;
348
+ }
349
+ }
350
+ }
351
+
352
+ module.exports = HookMetadataService;