Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
183 lines
4.6 KiB
JavaScript
Executable File
183 lines
4.6 KiB
JavaScript
Executable File
#!/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();
|