feat: auto-implement design changes via post-commit hook
- Add scripts/design-diff-analyzer.js to detect design changes • Analyzes ui-design.pen diff (colors, typography, spacing, layout) • Generates implementation tasks for Claude Code • Distinguishes content-only changes (no implementation) - Add .git/hooks/post-commit to spawn Claude Code automatically • Triggers when ui-design.pen is committed • Spawns isolated sub-agent with auto-notify on completion • 10min timeout, auto-cleanup after done - Update install-hooks.sh to install post-commit hook - Document full workflow in .github/HOOKS.md Workflow: commit design → analyzer runs → Claude Code spawns → implements → notifies Brian → Brian notifies Luca
This commit is contained in:
190
scripts/design-diff-analyzer.js
Executable file
190
scripts/design-diff-analyzer.js
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Design Diff Analyzer
|
||||
* Analyzes changes to ui-design.pen and generates implementation tasks for Claude Code
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
function getDiff() {
|
||||
try {
|
||||
return execSync('git diff HEAD~1 ui-design.pen', { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function analyzeDiff(diff) {
|
||||
const changes = {
|
||||
colors: [],
|
||||
typography: [],
|
||||
spacing: [],
|
||||
layout: [],
|
||||
content: []
|
||||
};
|
||||
|
||||
const lines = diff.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Color changes
|
||||
if (line.includes('"fill":') || line.includes('"backgroundColor":')) {
|
||||
const oldMatch = lines[i-1]?.match(/"(fill|backgroundColor)":\s*"([^"]+)"/);
|
||||
const newMatch = line.match(/"(fill|backgroundColor)":\s*"([^"]+)"/);
|
||||
|
||||
if (oldMatch && newMatch && oldMatch[2] !== newMatch[2]) {
|
||||
changes.colors.push({
|
||||
type: oldMatch[1],
|
||||
from: oldMatch[2],
|
||||
to: newMatch[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Font size changes
|
||||
if (line.includes('"fontSize":')) {
|
||||
const oldMatch = lines[i-1]?.match(/"fontSize":\s*(\d+)/);
|
||||
const newMatch = line.match(/"fontSize":\s*(\d+)/);
|
||||
|
||||
if (oldMatch && newMatch && oldMatch[1] !== newMatch[1]) {
|
||||
changes.typography.push({
|
||||
type: 'fontSize',
|
||||
from: parseInt(oldMatch[1]),
|
||||
to: parseInt(newMatch[1])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Spacing/padding changes
|
||||
if (line.includes('"padding":') || line.includes('"margin":') || line.includes('"gap":')) {
|
||||
const oldMatch = lines[i-1]?.match(/"(padding|margin|gap)":\s*(\d+)/);
|
||||
const newMatch = line.match(/"(padding|margin|gap)":\s*(\d+)/);
|
||||
|
||||
if (oldMatch && newMatch && oldMatch[2] !== newMatch[2]) {
|
||||
changes.spacing.push({
|
||||
type: oldMatch[1],
|
||||
from: parseInt(oldMatch[2]),
|
||||
to: parseInt(newMatch[2])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Content changes (text only - no implementation needed)
|
||||
if (line.includes('"content":')) {
|
||||
const oldMatch = lines[i-1]?.match(/"content":\s*"([^"]+)"/);
|
||||
const newMatch = line.match(/"content":\s*"([^"]+)"/);
|
||||
|
||||
if (oldMatch && newMatch && oldMatch[1] !== newMatch[1]) {
|
||||
changes.content.push({
|
||||
from: oldMatch[1],
|
||||
to: newMatch[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function generateTasks(changes) {
|
||||
const tasks = [];
|
||||
|
||||
// Color changes → update theme.json or CSS variables
|
||||
if (changes.colors.length > 0) {
|
||||
const colorList = changes.colors.map(c =>
|
||||
` - ${c.type}: ${c.from} → ${c.to}`
|
||||
).join('\n');
|
||||
|
||||
tasks.push({
|
||||
priority: 'high',
|
||||
description: 'Update color palette',
|
||||
details: `Color changes detected:\n${colorList}\n\nUpdate src/theme.json or CSS variables to match the design.`
|
||||
});
|
||||
}
|
||||
|
||||
// Typography changes → update theme.json
|
||||
if (changes.typography.length > 0) {
|
||||
const typoList = changes.typography.map(t =>
|
||||
` - ${t.type}: ${t.from}px → ${t.to}px`
|
||||
).join('\n');
|
||||
|
||||
tasks.push({
|
||||
priority: 'medium',
|
||||
description: 'Update typography',
|
||||
details: `Typography changes detected:\n${typoList}\n\nUpdate src/theme.json typography settings.`
|
||||
});
|
||||
}
|
||||
|
||||
// Spacing changes → update theme.json
|
||||
if (changes.spacing.length > 0) {
|
||||
const spacingList = changes.spacing.map(s =>
|
||||
` - ${s.type}: ${s.from}px → ${s.to}px`
|
||||
).join('\n');
|
||||
|
||||
tasks.push({
|
||||
priority: 'medium',
|
||||
description: 'Update spacing',
|
||||
details: `Spacing changes detected:\n${spacingList}\n\nUpdate src/theme.json spacing values.`
|
||||
});
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
function formatOutput(tasks, changes) {
|
||||
if (tasks.length === 0 && changes.content.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let output = '🎨 Design changes detected:\n\n';
|
||||
|
||||
if (tasks.length > 0) {
|
||||
output += '📋 Implementation tasks:\n\n';
|
||||
tasks.forEach((task, i) => {
|
||||
output += `${i + 1}. [${task.priority.toUpperCase()}] ${task.description}\n`;
|
||||
output += `${task.details}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (changes.content.length > 0) {
|
||||
output += '📝 Content changes (mockup text only, no code changes needed):\n';
|
||||
changes.content.forEach(c => {
|
||||
output += ` - "${c.from}" → "${c.to}"\n`;
|
||||
});
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
return { output, tasks };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const diff = getDiff();
|
||||
|
||||
if (!diff) {
|
||||
console.log('No design changes detected');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const changes = analyzeDiff(diff);
|
||||
const tasks = generateTasks(changes);
|
||||
const result = formatOutput(tasks, changes);
|
||||
|
||||
if (!result) {
|
||||
console.log('No significant design changes detected');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(result.output);
|
||||
|
||||
// Output JSON for automation
|
||||
if (process.argv.includes('--json')) {
|
||||
console.log('\n---JSON---');
|
||||
console.log(JSON.stringify({ tasks, changes }, null, 2));
|
||||
}
|
||||
|
||||
process.exit(tasks.length > 0 ? 1 : 0); // Exit 1 if there are tasks to implement
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user