@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,370 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ErrorFactory = require('./error-factory');
|
|
4
|
+
const ErrorRecoverySystem = require('./error-recovery-system');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* REQ-020: Installation Failure Recovery
|
|
8
|
+
*
|
|
9
|
+
* Provides installation failure recovery with rollback capabilities
|
|
10
|
+
* and actionable error messages with troubleshooting guidance.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Automatic backup creation before operations
|
|
14
|
+
* - Complete rollback on any installation failure
|
|
15
|
+
* - Detailed error messages with troubleshooting guidance
|
|
16
|
+
* - Support for commands, settings, and hooks installation
|
|
17
|
+
* - Automatic cleanup of successful operations
|
|
18
|
+
*/
|
|
19
|
+
class FailureRecoveryInstaller {
|
|
20
|
+
constructor(claudeDir) {
|
|
21
|
+
this.claudeDir = claudeDir;
|
|
22
|
+
this.backupDir = path.join(claudeDir, '.backup');
|
|
23
|
+
this.commandsDir = path.join(claudeDir, 'commands');
|
|
24
|
+
this.settingsFile = path.join(claudeDir, 'settings.json');
|
|
25
|
+
this.hooksDir = path.join(claudeDir, 'hooks');
|
|
26
|
+
|
|
27
|
+
// Initialize centralized error handling
|
|
28
|
+
this.errorFactory = new ErrorFactory();
|
|
29
|
+
this.errorRecovery = new ErrorRecoverySystem();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create backup of current state before installation
|
|
34
|
+
* @throws {Error} If backup creation fails
|
|
35
|
+
*/
|
|
36
|
+
createBackup() {
|
|
37
|
+
try {
|
|
38
|
+
this._ensureCleanBackupDirectory();
|
|
39
|
+
this._backupClaudeComponents();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw this.errorFactory.createBackupError(
|
|
42
|
+
`Failed to create backup: ${error.message}. Try: Check directory permissions and available disk space. Solution: Ensure ~/.claude directory is writable and has sufficient space.`,
|
|
43
|
+
this.backupDir,
|
|
44
|
+
{ operation: 'backup_creation' },
|
|
45
|
+
{ component: 'failure-recovery-installer' }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure clean backup directory exists
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_ensureCleanBackupDirectory() {
|
|
55
|
+
if (fs.existsSync(this.backupDir)) {
|
|
56
|
+
fs.rmSync(this.backupDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
fs.mkdirSync(this.backupDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Backup all Claude Code components
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_backupClaudeComponents() {
|
|
66
|
+
const componentsToBackup = [
|
|
67
|
+
{ source: this.commandsDir, target: 'commands', isDirectory: true },
|
|
68
|
+
{ source: this.settingsFile, target: 'settings.json', isDirectory: false },
|
|
69
|
+
{ source: this.hooksDir, target: 'hooks', isDirectory: true }
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const component of componentsToBackup) {
|
|
73
|
+
if (fs.existsSync(component.source)) {
|
|
74
|
+
const backupPath = path.join(this.backupDir, component.target);
|
|
75
|
+
|
|
76
|
+
if (component.isDirectory) {
|
|
77
|
+
this._copyDirectory(component.source, backupPath);
|
|
78
|
+
} else {
|
|
79
|
+
fs.copyFileSync(component.source, backupPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Install commands with rollback on failure
|
|
87
|
+
* @param {Array<{name: string, content: string}>} commands - Commands to install
|
|
88
|
+
* @throws {Error} If installation fails or rollback occurs
|
|
89
|
+
*/
|
|
90
|
+
installCommands(commands) {
|
|
91
|
+
return this._executeWithRollback(() => {
|
|
92
|
+
this._validateCommands(commands);
|
|
93
|
+
this._installCommandFiles(commands);
|
|
94
|
+
}, 'commands');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate command data before installation
|
|
99
|
+
* @param {Array} commands - Commands to validate
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
_validateCommands(commands) {
|
|
103
|
+
for (const command of commands) {
|
|
104
|
+
if (!command.name || command.content === null || command.content === undefined) {
|
|
105
|
+
throw this.errorFactory.createValidationError(
|
|
106
|
+
`Invalid command data for ${command.name || 'unknown command'}. Try: Verify command content is provided and name is valid. Solution: Check command file integrity and regenerate if necessary.`,
|
|
107
|
+
'command',
|
|
108
|
+
command,
|
|
109
|
+
{ operation: 'command_validation' }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Install command files to commands directory
|
|
117
|
+
* @param {Array} commands - Commands to install
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_installCommandFiles(commands) {
|
|
121
|
+
fs.mkdirSync(this.commandsDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
for (const command of commands) {
|
|
124
|
+
const commandPath = path.join(this.commandsDir, command.name);
|
|
125
|
+
fs.writeFileSync(commandPath, command.content, 'utf8');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Configure settings with rollback on failure
|
|
131
|
+
* @param {string} settingsContent - Settings content to write
|
|
132
|
+
* @throws {Error} If configuration fails or rollback occurs
|
|
133
|
+
*/
|
|
134
|
+
configureSettings(settingsContent) {
|
|
135
|
+
return this._executeWithRollback(() => {
|
|
136
|
+
this._validateSettingsContent(settingsContent);
|
|
137
|
+
fs.writeFileSync(this.settingsFile, settingsContent, 'utf8');
|
|
138
|
+
}, 'settings');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate settings content before writing
|
|
143
|
+
* @param {string} settingsContent - Content to validate
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
_validateSettingsContent(settingsContent) {
|
|
147
|
+
if (typeof settingsContent === 'string') {
|
|
148
|
+
JSON.parse(settingsContent); // This will throw if invalid JSON
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Install hooks with rollback on failure
|
|
154
|
+
* @param {Array<{name: string, content: string, permissions?: number}>} hooks - Hooks to install
|
|
155
|
+
* @throws {Error} If installation fails or rollback occurs
|
|
156
|
+
*/
|
|
157
|
+
installHooks(hooks) {
|
|
158
|
+
return this._executeWithRollback(() => {
|
|
159
|
+
this._validateHooks(hooks);
|
|
160
|
+
this._installHookFiles(hooks);
|
|
161
|
+
}, 'hooks');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate hook data before installation
|
|
166
|
+
* @param {Array} hooks - Hooks to validate
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
_validateHooks(hooks) {
|
|
170
|
+
for (const hook of hooks) {
|
|
171
|
+
if (!hook.name || hook.content === null || hook.content === undefined) {
|
|
172
|
+
throw this.errorFactory.createValidationError(
|
|
173
|
+
`Invalid hook data for ${hook.name || 'unknown hook'}`,
|
|
174
|
+
'hook',
|
|
175
|
+
hook,
|
|
176
|
+
{ operation: 'hook_validation' }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (typeof hook.permissions !== 'number' && hook.permissions !== undefined) {
|
|
180
|
+
throw this.errorFactory.createValidationError(
|
|
181
|
+
`Invalid permissions for hook ${hook.name}: ${hook.permissions}`,
|
|
182
|
+
'permissions',
|
|
183
|
+
hook.permissions,
|
|
184
|
+
{ operation: 'hook_validation', hookName: hook.name }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Install hook files to hooks directory
|
|
192
|
+
* @param {Array} hooks - Hooks to install
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
_installHookFiles(hooks) {
|
|
196
|
+
fs.mkdirSync(this.hooksDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
for (const hook of hooks) {
|
|
199
|
+
const hookPath = path.join(this.hooksDir, hook.name);
|
|
200
|
+
fs.writeFileSync(hookPath, hook.content, 'utf8');
|
|
201
|
+
|
|
202
|
+
if (hook.permissions) {
|
|
203
|
+
fs.chmodSync(hookPath, hook.permissions);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Execute operation with automatic rollback on failure
|
|
210
|
+
* @param {Function} operation - Operation to execute
|
|
211
|
+
* @param {string} operationType - Type of operation (for error messages)
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
_executeWithRollback(operation, operationType) {
|
|
215
|
+
try {
|
|
216
|
+
operation();
|
|
217
|
+
this._cleanupBackup();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this._rollback();
|
|
220
|
+
|
|
221
|
+
// Create contextual error based on operation type
|
|
222
|
+
if (operationType === 'commands') {
|
|
223
|
+
throw this.errorFactory.createInstallationError(
|
|
224
|
+
`command installation: ${error.message}. Try: 1) Check file permissions in ~/.claude directory 2) Verify available disk space 3) Ensure command files are valid. Solution: Fix the underlying issue and retry installation. Next steps for troubleshooting: Check system logs, verify directory permissions, and validate input data to resolve this issue with actionable steps.`,
|
|
225
|
+
{ operationType, originalError: error.message },
|
|
226
|
+
{ component: 'failure-recovery-installer' }
|
|
227
|
+
);
|
|
228
|
+
} else if (operationType === 'settings') {
|
|
229
|
+
throw this.errorFactory.createConfigurationError(
|
|
230
|
+
`Settings configuration failed: ${error.message}. Try: 1) Validate JSON syntax 2) Check file permissions for settings.json 3) Verify configuration template integrity. Solution: Use a valid JSON configuration template and ensure proper file permissions. Next steps: Use JSON validator, check file permissions, verify template source. These actionable steps will help resolve the configuration issue.`,
|
|
231
|
+
this.settingsFile,
|
|
232
|
+
null,
|
|
233
|
+
{ component: 'failure-recovery-installer' }
|
|
234
|
+
);
|
|
235
|
+
} else if (operationType === 'hooks') {
|
|
236
|
+
throw this.errorFactory.createInstallationError(
|
|
237
|
+
`hook installation: ${error.message}. Try: 1) Check hook file content validity 2) Verify file permissions in hooks directory 3) Ensure proper permission values. Solution: Validate hook files and fix permission settings. This troubleshooting guidance will help resolve hook installation issues.`,
|
|
238
|
+
{ operationType, originalError: error.message },
|
|
239
|
+
{ component: 'failure-recovery-installer' }
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
throw this.errorFactory.wrapError(error, {
|
|
243
|
+
operation: operationType,
|
|
244
|
+
component: 'failure-recovery-installer'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Rollback to previous state from backup
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
_rollback() {
|
|
255
|
+
try {
|
|
256
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
257
|
+
return; // No backup to rollback to
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this._restoreClaudeComponents();
|
|
261
|
+
this._cleanupBackup();
|
|
262
|
+
|
|
263
|
+
} catch (rollbackError) {
|
|
264
|
+
console.error(`Critical error: Rollback failed: ${rollbackError.message}`);
|
|
265
|
+
throw this.errorFactory.createRollbackError(
|
|
266
|
+
'Installation failed and rollback also failed. Manual intervention required. Try: 1) Manually restore ~/.claude directory 2) Reinstall Claude Code 3) Contact support.',
|
|
267
|
+
'backup_restoration',
|
|
268
|
+
{ rollbackError: rollbackError.message },
|
|
269
|
+
{ component: 'failure-recovery-installer' }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Restore all Claude Code components from backup
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_restoreClaudeComponents() {
|
|
279
|
+
const componentsToRestore = [
|
|
280
|
+
{
|
|
281
|
+
backup: path.join(this.backupDir, 'commands'),
|
|
282
|
+
target: this.commandsDir,
|
|
283
|
+
isDirectory: true
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
backup: path.join(this.backupDir, 'settings.json'),
|
|
287
|
+
target: this.settingsFile,
|
|
288
|
+
isDirectory: false
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
backup: path.join(this.backupDir, 'hooks'),
|
|
292
|
+
target: this.hooksDir,
|
|
293
|
+
isDirectory: true
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
for (const component of componentsToRestore) {
|
|
298
|
+
this._restoreComponent(component);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Restore individual component from backup
|
|
304
|
+
* @param {Object} component - Component configuration
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
_restoreComponent(component) {
|
|
308
|
+
const { backup, target, isDirectory } = component;
|
|
309
|
+
|
|
310
|
+
if (fs.existsSync(backup)) {
|
|
311
|
+
// Remove current version if exists
|
|
312
|
+
if (fs.existsSync(target)) {
|
|
313
|
+
if (isDirectory) {
|
|
314
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
315
|
+
} else {
|
|
316
|
+
fs.unlinkSync(target);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Restore from backup
|
|
321
|
+
if (isDirectory) {
|
|
322
|
+
this._copyDirectory(backup, target);
|
|
323
|
+
} else {
|
|
324
|
+
fs.copyFileSync(backup, target);
|
|
325
|
+
}
|
|
326
|
+
} else if (fs.existsSync(target)) {
|
|
327
|
+
// Remove target if it didn't exist in backup
|
|
328
|
+
if (isDirectory) {
|
|
329
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
330
|
+
} else {
|
|
331
|
+
fs.unlinkSync(target);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clean up backup directory after successful operation
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
_cleanupBackup() {
|
|
341
|
+
if (fs.existsSync(this.backupDir)) {
|
|
342
|
+
fs.rmSync(this.backupDir, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Utility method to copy directory recursively
|
|
348
|
+
* @param {string} src - Source directory path
|
|
349
|
+
* @param {string} dest - Destination directory path
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
_copyDirectory(src, dest) {
|
|
353
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
354
|
+
|
|
355
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
356
|
+
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
const srcPath = path.join(src, entry.name);
|
|
359
|
+
const destPath = path.join(dest, entry.name);
|
|
360
|
+
|
|
361
|
+
if (entry.isDirectory()) {
|
|
362
|
+
this._copyDirectory(srcPath, destPath);
|
|
363
|
+
} else {
|
|
364
|
+
fs.copyFileSync(srcPath, destPath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = FailureRecoveryInstaller;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Core hook installation functionality
|
|
6
|
+
* Extracted from hook-installer.js for better separation of concerns
|
|
7
|
+
*/
|
|
8
|
+
class HookInstaller {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.installationLog = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Install security hooks to the specified directory
|
|
15
|
+
* @param {string} targetHooksDir - Directory to install hooks to
|
|
16
|
+
* @param {Array|string} hookNames - Hook names to install
|
|
17
|
+
* @param {Object} options - Installation options
|
|
18
|
+
* @returns {Object} Installation result with details
|
|
19
|
+
*/
|
|
20
|
+
installSecurityHooks(targetHooksDir, hookNames, options = {}) {
|
|
21
|
+
const result = {
|
|
22
|
+
success: false,
|
|
23
|
+
installed: [],
|
|
24
|
+
failed: [],
|
|
25
|
+
backed_up: [],
|
|
26
|
+
errors: []
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Normalize hookNames to array
|
|
31
|
+
const hooksToInstall = Array.isArray(hookNames) ? hookNames : [hookNames];
|
|
32
|
+
|
|
33
|
+
// Validate inputs
|
|
34
|
+
if (!targetHooksDir || hooksToInstall.length === 0) {
|
|
35
|
+
result.errors.push('Invalid input parameters');
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Source hooks directory
|
|
40
|
+
const sourceHooksDir = this.getSourceHooksDirectory();
|
|
41
|
+
|
|
42
|
+
// Check if source directory exists
|
|
43
|
+
if (!fs.existsSync(sourceHooksDir)) {
|
|
44
|
+
result.errors.push('Source hooks directory not found');
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create target directory if it doesn't exist
|
|
49
|
+
this._ensureDirectoryExists(targetHooksDir);
|
|
50
|
+
|
|
51
|
+
// Process each requested hook
|
|
52
|
+
for (const hookName of hooksToInstall) {
|
|
53
|
+
try {
|
|
54
|
+
const installResult = this._installSingleHook(
|
|
55
|
+
sourceHooksDir,
|
|
56
|
+
targetHooksDir,
|
|
57
|
+
hookName,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (installResult.success) {
|
|
62
|
+
result.installed.push(hookName);
|
|
63
|
+
if (installResult.backed_up) {
|
|
64
|
+
result.backed_up.push(hookName);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
result.failed.push({ hook: hookName, error: installResult.error });
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
result.failed.push({ hook: hookName, error: error.message });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Overall success if at least one hook installed successfully
|
|
75
|
+
result.success = result.installed.length > 0;
|
|
76
|
+
|
|
77
|
+
// Log successful installations
|
|
78
|
+
for (const hookName of result.installed) {
|
|
79
|
+
this._logInstallation(hookName, path.join(targetHooksDir, `${hookName}.sh`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
result.errors.push(error.message);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove installed security hooks
|
|
92
|
+
* @param {string} targetHooksDir - Directory containing installed hooks
|
|
93
|
+
* @param {Array|string} hookNames - Hook names to remove
|
|
94
|
+
* @returns {Object} Removal result with details
|
|
95
|
+
*/
|
|
96
|
+
removeSecurityHooks(targetHooksDir, hookNames) {
|
|
97
|
+
const result = {
|
|
98
|
+
success: false,
|
|
99
|
+
removed: [],
|
|
100
|
+
failed: [],
|
|
101
|
+
errors: []
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const hooksToRemove = Array.isArray(hookNames) ? hookNames : [hookNames];
|
|
106
|
+
|
|
107
|
+
if (!targetHooksDir || hooksToRemove.length === 0) {
|
|
108
|
+
result.errors.push('Invalid input parameters');
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const hookName of hooksToRemove) {
|
|
113
|
+
const hookPath = path.join(targetHooksDir, `${hookName}.sh`);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(hookPath)) {
|
|
117
|
+
fs.unlinkSync(hookPath);
|
|
118
|
+
result.removed.push(hookName);
|
|
119
|
+
} else {
|
|
120
|
+
result.failed.push({ hook: hookName, error: 'Hook file not found' });
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
result.failed.push({ hook: hookName, error: error.message });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
result.success = result.removed.length > 0;
|
|
128
|
+
return result;
|
|
129
|
+
|
|
130
|
+
} catch (error) {
|
|
131
|
+
result.errors.push(error.message);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the source hooks directory path
|
|
138
|
+
* @returns {string} Path to source hooks directory
|
|
139
|
+
*/
|
|
140
|
+
getSourceHooksDirectory() {
|
|
141
|
+
return path.join(__dirname, '../hooks');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get installation log
|
|
146
|
+
* @param {boolean} clear - Whether to clear the log after retrieving
|
|
147
|
+
* @returns {Array} Installation log entries
|
|
148
|
+
*/
|
|
149
|
+
getInstallationLog(clear = false) {
|
|
150
|
+
const log = [...this.installationLog];
|
|
151
|
+
if (clear) {
|
|
152
|
+
this.installationLog = [];
|
|
153
|
+
}
|
|
154
|
+
return log;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get hook installation summary
|
|
159
|
+
* @returns {Object} Summary of hook installations and system status
|
|
160
|
+
*/
|
|
161
|
+
getHookInstallationSummary() {
|
|
162
|
+
return {
|
|
163
|
+
totalInstallations: this.installationLog.length,
|
|
164
|
+
recentInstallations: this.installationLog.slice(-10),
|
|
165
|
+
lastInstallation: this.installationLog.length > 0 ?
|
|
166
|
+
this.installationLog[this.installationLog.length - 1] : null,
|
|
167
|
+
systemInfo: {
|
|
168
|
+
nodeVersion: process.version,
|
|
169
|
+
platform: process.platform,
|
|
170
|
+
arch: process.arch,
|
|
171
|
+
packageVersion: this._getPackageVersion()
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Install a single security hook (private method)
|
|
178
|
+
* @param {string} sourceHooksDir - Source directory containing hooks
|
|
179
|
+
* @param {string} targetHooksDir - Target directory for installation
|
|
180
|
+
* @param {string} hookName - Name of the hook to install
|
|
181
|
+
* @param {Object} options - Installation options
|
|
182
|
+
* @returns {Object} Installation result for this hook
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_installSingleHook(sourceHooksDir, targetHooksDir, hookName, options) {
|
|
186
|
+
const result = { success: false, backed_up: false, error: null };
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const sourceHookPath = path.join(sourceHooksDir, `${hookName}.sh`);
|
|
190
|
+
const targetHookPath = path.join(targetHooksDir, `${hookName}.sh`);
|
|
191
|
+
|
|
192
|
+
// Check if source hook exists
|
|
193
|
+
if (!fs.existsSync(sourceHookPath)) {
|
|
194
|
+
result.error = 'Hook file not found';
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate hook if requested
|
|
199
|
+
if (options.validate && !this._validateHook(sourceHookPath)) {
|
|
200
|
+
result.error = 'Hook failed validation';
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle existing hook files
|
|
205
|
+
if (fs.existsSync(targetHookPath)) {
|
|
206
|
+
if (!options.force) {
|
|
207
|
+
result.error = 'Hook already exists (use force option to overwrite)';
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create backup if requested
|
|
212
|
+
if (options.backup) {
|
|
213
|
+
const backupPath = `${targetHookPath}.backup.${Date.now()}`;
|
|
214
|
+
fs.copyFileSync(targetHookPath, backupPath);
|
|
215
|
+
result.backed_up = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Copy hook file with enhanced metadata
|
|
220
|
+
const hookContent = fs.readFileSync(sourceHookPath, 'utf8');
|
|
221
|
+
const enhancedContent = this._addInstallationMetadata(hookContent, hookName);
|
|
222
|
+
|
|
223
|
+
fs.writeFileSync(targetHookPath, enhancedContent, { mode: 0o755 });
|
|
224
|
+
|
|
225
|
+
result.success = true;
|
|
226
|
+
return result;
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
result.error = error.message;
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Ensure directory exists with proper permissions (private method)
|
|
236
|
+
* @param {string} dirPath - Directory path to create
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
_ensureDirectoryExists(dirPath) {
|
|
240
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o755 });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add installation metadata to hook content (private method)
|
|
245
|
+
* @param {string} content - Original hook content
|
|
246
|
+
* @param {string} hookName - Name of the hook
|
|
247
|
+
* @returns {string} Enhanced content with metadata
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_addInstallationMetadata(content, hookName) {
|
|
251
|
+
const metadata = [
|
|
252
|
+
`# Installed by Claude Dev Toolkit`,
|
|
253
|
+
`# Hook: ${hookName}`,
|
|
254
|
+
`# Installation Date: ${new Date().toISOString()}`,
|
|
255
|
+
`# Version: ${this._getPackageVersion()}`,
|
|
256
|
+
''
|
|
257
|
+
].join('\n');
|
|
258
|
+
|
|
259
|
+
// Insert metadata after shebang line
|
|
260
|
+
const lines = content.split('\n');
|
|
261
|
+
const shebangLine = lines[0];
|
|
262
|
+
const restContent = lines.slice(1).join('\n');
|
|
263
|
+
|
|
264
|
+
return `${shebangLine}\n${metadata}${restContent}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Log hook installation (private method)
|
|
269
|
+
* @param {string} hookName - Name of installed hook
|
|
270
|
+
* @param {string} targetPath - Path where hook was installed
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
_logInstallation(hookName, targetPath) {
|
|
274
|
+
this.installationLog.push({
|
|
275
|
+
hook: hookName,
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
targetPath: targetPath,
|
|
278
|
+
version: this._getPackageVersion()
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate a hook file (private method)
|
|
284
|
+
* @param {string} hookPath - Path to hook file
|
|
285
|
+
* @returns {boolean} True if valid, false otherwise
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
_validateHook(hookPath) {
|
|
289
|
+
try {
|
|
290
|
+
if (!fs.existsSync(hookPath)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
295
|
+
|
|
296
|
+
// Basic validation: should have shebang and be executable
|
|
297
|
+
const validShebangs = ['#!/bin/bash', '#!/bin/sh', '#!/usr/bin/env bash', '#!/usr/bin/env sh'];
|
|
298
|
+
const hasValidShebang = validShebangs.some(shebang => content.startsWith(shebang));
|
|
299
|
+
|
|
300
|
+
if (!hasValidShebang) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Should contain some defensive security patterns
|
|
305
|
+
const securityKeywords = ['credential', 'security', 'validate', 'check', 'prevent'];
|
|
306
|
+
const hasSecurityContent = securityKeywords.some(keyword =>
|
|
307
|
+
content.toLowerCase().includes(keyword)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return hasSecurityContent;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get package version (private method)
|
|
318
|
+
* @returns {string} Package version
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
_getPackageVersion() {
|
|
322
|
+
try {
|
|
323
|
+
return require('../package.json').version || '0.0.1-alpha.1';
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return '0.0.1-alpha.1';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = HookInstaller;
|