Skip to main content
Glama
hooks-system.jsโ€ข8.2 kB
/** * Hooks System for MCP SSH Manager * Provides automation through pre/post execution hooks */ import fs from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import { fileURLToPath } from 'url'; import { loadProfile } from './profile-loader.js'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const HOOKS_CONFIG_FILE = path.join(__dirname, '..', '.hooks-config.json'); const HOOKS_DIR = path.join(__dirname, '..', 'hooks'); // Get hooks from the active profile let profileHooks = {}; try { const profile = loadProfile(); profileHooks = profile.hooks || {}; } catch (error) { console.error(`Error loading profile hooks: ${error.message}`); } // Default hooks configuration (minimal, can be overridden by profiles) const DEFAULT_HOOKS = { // Basic error handling 'on-error': { enabled: true, description: 'Run when an error occurs', actions: [ { type: 'notification', name: 'log-error', command: 'echo "[$(date)] Error on {server}: {error}" >> errors.log' } ] }, // SSH key change hooks 'pre-connect-key-change': { enabled: false, description: 'Run before accepting a changed SSH host key', actions: [ { type: 'notification', name: 'log-key-change', command: 'echo "[$(date)] SSH key change detected for {server} ({host}:{port})" >> ssh-key-changes.log' } ] }, 'post-key-update': { enabled: false, description: 'Run after updating an SSH host key', actions: [ { type: 'notification', name: 'log-key-updated', command: 'echo "[$(date)] SSH key {action} for {server} ({host}:{port})" >> ssh-key-changes.log' } ] } }; /** * Initialize hooks directory and configuration */ export async function initializeHooks() { // Create hooks directory if it doesn't exist if (!fs.existsSync(HOOKS_DIR)) { fs.mkdirSync(HOOKS_DIR, { recursive: true }); } // Merge profile hooks with defaults const mergedHooks = { ...DEFAULT_HOOKS, ...profileHooks }; // Create hooks configuration if it doesn't exist if (!fs.existsSync(HOOKS_CONFIG_FILE)) { saveHooksConfig(mergedHooks); } return true; } /** * Load hooks configuration */ export function loadHooksConfig() { try { // Start with profile hooks let hooks = { ...profileHooks }; // Merge with custom hooks from file if (fs.existsSync(HOOKS_CONFIG_FILE)) { const data = fs.readFileSync(HOOKS_CONFIG_FILE, 'utf8'); const customHooks = JSON.parse(data); // Deep merge hooks for (const [hookName, hookConfig] of Object.entries(customHooks)) { if (hooks[hookName]) { // Merge existing hook hooks[hookName] = { ...hooks[hookName], ...hookConfig }; } else { // Add new hook hooks[hookName] = hookConfig; } } } return hooks; } catch (error) { console.error(`Error loading hooks config: ${error.message}`); } return { ...DEFAULT_HOOKS, ...profileHooks }; } /** * Save hooks configuration */ export function saveHooksConfig(config) { try { fs.writeFileSync(HOOKS_CONFIG_FILE, JSON.stringify(config, null, 2)); return true; } catch (error) { console.error(`Error saving hooks config: ${error.message}`); return false; } } /** * Execute a hook */ export async function executeHook(hookName, context = {}) { const config = loadHooksConfig(); const hook = config[hookName]; if (!hook || !hook.enabled) { return { success: true, skipped: true }; } console.error(`๐ŸŽฃ Executing hook: ${hookName}`); const results = []; for (const action of hook.actions) { try { // Check environment variables if required if (action.requiresEnv) { const missingEnv = action.requiresEnv.filter(env => !process.env[env]); if (missingEnv.length > 0) { if (!action.optional) { throw new Error(`Missing required environment variables: ${missingEnv.join(', ')}`); } console.error(` โš ๏ธ Skipping ${action.name}: missing env vars`); continue; } } // Replace placeholders in command let command = action.command || action.remoteCommand || ''; for (const [key, value] of Object.entries(context)) { command = command.replace(new RegExp(`{${key}}`, 'g'), value); } // Execute action based on type const result = await executeAction(action, command, context); results.push(result); // Check validation results if (action.type === 'validation' && !result.success && !action.optional) { const errorMsg = action.errorMessage || `Validation failed: ${action.name}`; throw new Error(errorMsg); } console.error(` โœ… ${action.name}: completed`); } catch (error) { if (!action.optional) { console.error(` โŒ ${action.name}: ${error.message}`); return { success: false, hook: hookName, action: action.name, error: error.message, results }; } console.error(` โš ๏ธ ${action.name}: ${error.message} (optional, continuing)`); } } return { success: true, hook: hookName, results }; } /** * Execute a single action */ async function executeAction(action, command, context) { const result = { action: action.name, type: action.type, timestamp: new Date().toISOString() }; try { if (action.remoteCommand && context.sshConnection) { // Execute on remote server const output = await context.sshConnection.execCommand(command, { cwd: context.cwd || context.defaultDir }); result.output = output.stdout; result.error = output.stderr; result.success = output.code === 0; if (action.expectEmpty && output.stdout.trim()) { result.success = false; } } else if (action.command) { // Execute locally const { stdout, stderr } = await execAsync(command); result.output = stdout; result.error = stderr; result.success = true; if (action.expectEmpty && stdout.trim()) { result.success = false; } } // Handle specific action types switch (action.type) { case 'backup': result.backupInfo = { timestamp: new Date().toISOString(), command: command }; break; case 'notification': result.notified = true; break; case 'validation': result.validated = result.success; break; case 'verification': result.verified = result.success; break; } } catch (error) { result.success = false; result.error = error.message; } return result; } /** * Add or update a hook */ export function addHook(hookName, hookConfig) { const config = loadHooksConfig(); config[hookName] = hookConfig; return saveHooksConfig(config); } /** * Remove a hook */ export function removeHook(hookName) { const config = loadHooksConfig(); delete config[hookName]; return saveHooksConfig(config); } /** * Enable/disable a hook */ export function toggleHook(hookName, enabled) { const config = loadHooksConfig(); if (config[hookName]) { config[hookName].enabled = enabled; return saveHooksConfig(config); } return false; } /** * List all hooks */ export function listHooks() { const config = loadHooksConfig(); return Object.entries(config).map(([name, hook]) => ({ name, enabled: hook.enabled, description: hook.description, actionCount: hook.actions ? hook.actions.length : 0 })); } /** * Create a custom hook script */ export async function createHookScript(scriptName, scriptContent) { const scriptPath = path.join(HOOKS_DIR, scriptName); try { fs.writeFileSync(scriptPath, scriptContent); fs.chmodSync(scriptPath, '755'); return scriptPath; } catch (error) { throw new Error(`Failed to create hook script: ${error.message}`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bvisible/mcp-ssh-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server