@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.
- package/README.md +254 -0
- package/bin/claude-commands +132 -0
- package/lib/claude-code-compatibility.js +545 -0
- package/lib/command-selector.js +245 -0
- package/lib/config.js +182 -0
- package/lib/context-utils.js +80 -0
- package/lib/dependency-validator.js +354 -0
- package/lib/error-factory.js +394 -0
- package/lib/error-handler-utils.js +432 -0
- package/lib/error-recovery-system.js +563 -0
- package/lib/failure-recovery-installer.js +370 -0
- package/lib/hook-installer-core.js +330 -0
- package/lib/hook-installer.js +187 -0
- package/lib/hook-metadata-service.js +352 -0
- package/lib/hook-validator.js +358 -0
- package/lib/installation-configuration.js +380 -0
- package/lib/installation-instruction-generator.js +564 -0
- package/lib/installer.js +68 -0
- package/lib/package-manager-service.js +270 -0
- package/lib/permission-error-handler.js +543 -0
- package/lib/platform-utils.js +491 -0
- package/lib/setup-wizard-ui.js +245 -0
- package/lib/setup-wizard.js +355 -0
- package/lib/system-requirements-checker.js +558 -0
- package/lib/utils.js +15 -0
- package/lib/validation-utils.js +320 -0
- package/lib/version-validator-service.js +326 -0
- package/package.json +73 -0
- package/scripts/postinstall.js +182 -0
- package/scripts/validate.js +94 -0
|
@@ -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;
|