#!/usr/bin/env node /** * DSS Git Backup Hook * Automatically commits changes when Claude Code session ends. * Written from scratch for DSS. */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Configuration const DEFAULT_CONFIG = { git_backup: { enabled: true, require_git_repo: true, commit_only_if_changes: true, include_timestamp: true, commit_prefix: 'auto-backup', show_logs: false } }; // Prevent duplicate execution (Claude Code bug workaround) const STATE_DIR = path.join(__dirname, '..', '.state'); const LOCK_FILE = path.join(STATE_DIR, '.git-backup.lock'); const LOCK_TIMEOUT_MS = 3000; function loadConfig() { const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json'); try { if (fs.existsSync(configPath)) { const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); return { ...DEFAULT_CONFIG, ...userConfig }; } } catch (e) { // Use defaults } return DEFAULT_CONFIG; } function checkLock() { try { if (!fs.existsSync(STATE_DIR)) { fs.mkdirSync(STATE_DIR, { recursive: true }); } if (fs.existsSync(LOCK_FILE)) { const lastRun = parseInt(fs.readFileSync(LOCK_FILE, 'utf8')); if (!isNaN(lastRun) && (Date.now() - lastRun < LOCK_TIMEOUT_MS)) { return false; // Already ran recently } } fs.writeFileSync(LOCK_FILE, Date.now().toString(), 'utf8'); return true; } catch (e) { return true; // Proceed on error } } function isGitRepo() { try { execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' }); return true; } catch (e) { return false; } } function hasChanges() { try { const status = execSync('git status --porcelain', { encoding: 'utf8' }); return status.trim().length > 0; } catch (e) { return false; } } function getChangeSummary() { try { const status = execSync('git status --short', { encoding: 'utf8' }); const lines = status.trim().split('\n').filter(Boolean); let added = 0, modified = 0, deleted = 0; for (const line of lines) { const status = line.trim().charAt(0); if (status === 'A' || status === '?') added++; else if (status === 'M') modified++; else if (status === 'D') deleted++; } return { added, modified, deleted, total: lines.length }; } catch (e) { return { added: 0, modified: 0, deleted: 0, total: 0 }; } } function createBackup(config) { const backupConfig = config.git_backup || {}; try { // Stage all changes execSync('git add -A', { stdio: 'pipe' }); // Build commit message const parts = [backupConfig.commit_prefix || 'auto-backup']; if (backupConfig.include_timestamp) { const timestamp = new Date().toISOString().replace('T', ' ').replace(/\..+/, ''); parts.push(timestamp); } const summary = getChangeSummary(); const summaryText = `(${summary.total} files: +${summary.added} ~${summary.modified} -${summary.deleted})`; const commitMessage = `${parts.join(': ')} ${summaryText}\n\nGenerated by DSS Git Backup Hook`; // Create commit execSync(`git commit -m "${commitMessage}"`, { stdio: 'pipe' }); // Get commit hash const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); return { success: true, hash: commitHash, files: summary.total }; } catch (e) { return { success: false, error: e.message }; } } function log(config, message) { if (config.git_backup?.show_logs) { console.log(JSON.stringify({ systemMessage: message, continue: true })); } } function main() { // Prevent duplicate execution if (!checkLock()) { process.exit(0); } // Prevent hook recursion if (process.env.STOP_HOOK_ACTIVE === 'true') { process.exit(0); } const config = loadConfig(); if (!config.git_backup?.enabled) { process.exit(0); } // Check for git repo if (config.git_backup.require_git_repo && !isGitRepo()) { log(config, 'DSS Git Backup: Not a git repository, skipping'); process.exit(0); } // Check for changes if (config.git_backup.commit_only_if_changes && !hasChanges()) { log(config, 'DSS Git Backup: No changes to commit'); process.exit(0); } // Create backup const result = createBackup(config); if (result.success) { log(config, `DSS Git Backup: Committed ${result.files} files (${result.hash})`); } else { log(config, `DSS Git Backup: Failed - ${result.error}`); } process.exit(0); } main();