@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,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;