Initial commit
This commit is contained in:
267
skills/tdd-automation/utils/validate-claude-md.js
Normal file
267
skills/tdd-automation/utils/validate-claude-md.js
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* CLAUDE.md Validator
|
||||
*
|
||||
* Validates and manages CLAUDE.md files with backup and rollback capabilities.
|
||||
* Ensures safe installation of TDD automation without data loss.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class ClaudeMdValidator {
|
||||
constructor(projectRoot) {
|
||||
this.projectRoot = projectRoot;
|
||||
this.claudeDir = path.join(projectRoot, '.claude');
|
||||
this.claudeMdPath = path.join(this.claudeDir, 'CLAUDE.md');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate current CLAUDE.md state and determine installation strategy
|
||||
* @returns {object} Validation result with strategy recommendation
|
||||
*/
|
||||
validate() {
|
||||
const result = {
|
||||
exists: false,
|
||||
hasExistingContent: false,
|
||||
hasTddSection: false,
|
||||
needsBackup: false,
|
||||
strategy: 'create', // create | merge | skip | abort
|
||||
warnings: [],
|
||||
size: 0,
|
||||
path: this.claudeMdPath
|
||||
};
|
||||
|
||||
// Check if CLAUDE.md exists
|
||||
if (!fs.existsSync(this.claudeMdPath)) {
|
||||
result.strategy = 'create';
|
||||
result.warnings.push('No CLAUDE.md found - will create new file');
|
||||
return result;
|
||||
}
|
||||
|
||||
result.exists = true;
|
||||
const content = fs.readFileSync(this.claudeMdPath, 'utf-8');
|
||||
result.size = content.length;
|
||||
|
||||
// Check if file has meaningful content (not just empty or whitespace)
|
||||
if (content.trim().length > 0) {
|
||||
result.hasExistingContent = true;
|
||||
result.needsBackup = true;
|
||||
}
|
||||
|
||||
// Check if TDD automation is already installed
|
||||
if (this.detectTddSection(content)) {
|
||||
result.hasTddSection = true;
|
||||
result.strategy = 'skip';
|
||||
result.warnings.push('TDD automation already installed in CLAUDE.md');
|
||||
return result;
|
||||
}
|
||||
|
||||
// Determine merge strategy
|
||||
if (result.hasExistingContent) {
|
||||
result.strategy = 'merge';
|
||||
result.warnings.push(`Existing CLAUDE.md found (${result.size} bytes) - will merge`);
|
||||
} else {
|
||||
result.strategy = 'create';
|
||||
result.warnings.push('Empty CLAUDE.md found - will replace');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if TDD automation section exists in content
|
||||
* @param {string} content - CLAUDE.md content to check
|
||||
* @returns {boolean} True if TDD section detected
|
||||
*/
|
||||
detectTddSection(content) {
|
||||
const markers = [
|
||||
'<!-- TDD_AUTOMATION_START -->',
|
||||
'TDD Red-Green-Refactor Automation (Auto-Installed)',
|
||||
'tdd-automation-version:'
|
||||
];
|
||||
|
||||
return markers.some(marker => content.includes(marker));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamped backup of current CLAUDE.md
|
||||
* @returns {object} Backup result with success status and path
|
||||
*/
|
||||
createBackup() {
|
||||
if (!fs.existsSync(this.claudeMdPath)) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'No file to backup',
|
||||
path: null
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure .claude directory exists
|
||||
if (!fs.existsSync(this.claudeDir)) {
|
||||
fs.mkdirSync(this.claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = path.join(
|
||||
this.claudeDir,
|
||||
`CLAUDE.md.backup.${timestamp}`
|
||||
);
|
||||
|
||||
try {
|
||||
fs.copyFileSync(this.claudeMdPath, backupPath);
|
||||
const originalSize = fs.statSync(this.claudeMdPath).size;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: backupPath,
|
||||
originalSize,
|
||||
timestamp
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error.message,
|
||||
path: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all backup files sorted by date (newest first)
|
||||
* @returns {array} Array of backup file info objects
|
||||
*/
|
||||
listBackups() {
|
||||
if (!fs.existsSync(this.claudeDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.readdirSync(this.claudeDir)
|
||||
.filter(f => f.startsWith('CLAUDE.md.backup'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: path.join(this.claudeDir, f),
|
||||
created: fs.statSync(path.join(this.claudeDir, f)).mtime,
|
||||
size: fs.statSync(path.join(this.claudeDir, f)).size
|
||||
}))
|
||||
.sort((a, b) => b.created - a.created);
|
||||
} catch (error) {
|
||||
console.error('Error listing backups:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore CLAUDE.md from backup file
|
||||
* @param {string} backupPath - Path to backup file
|
||||
* @returns {object} Rollback result with success status
|
||||
*/
|
||||
rollback(backupPath) {
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'Backup file not found',
|
||||
path: backupPath
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure .claude directory exists
|
||||
if (!fs.existsSync(this.claudeDir)) {
|
||||
fs.mkdirSync(this.claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.copyFileSync(backupPath, this.claudeMdPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
restoredFrom: backupPath,
|
||||
size: fs.statSync(this.claudeMdPath).size
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error.message,
|
||||
path: backupPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove TDD section from CLAUDE.md
|
||||
* @returns {object} Removal result with success status
|
||||
*/
|
||||
removeTddSection() {
|
||||
if (!fs.existsSync(this.claudeMdPath)) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'CLAUDE.md not found'
|
||||
};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.claudeMdPath, 'utf-8');
|
||||
|
||||
// Check if TDD section exists
|
||||
if (!this.detectTddSection(content)) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'No TDD section found in CLAUDE.md'
|
||||
};
|
||||
}
|
||||
|
||||
// Create backup before removal
|
||||
const backup = this.createBackup();
|
||||
if (!backup.success) {
|
||||
return {
|
||||
success: false,
|
||||
reason: `Backup failed: ${backup.reason}`
|
||||
};
|
||||
}
|
||||
|
||||
// Remove TDD section using markers
|
||||
const startMarker = '<!-- TDD_AUTOMATION_START -->';
|
||||
const endMarker = '<!-- TDD_AUTOMATION_END -->';
|
||||
|
||||
const startIdx = content.indexOf(startMarker);
|
||||
const endIdx = content.indexOf(endMarker);
|
||||
|
||||
if (startIdx === -1 || endIdx === -1) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'Could not find section markers for clean removal'
|
||||
};
|
||||
}
|
||||
|
||||
// Remove section and clean up extra whitespace
|
||||
const beforeSection = content.substring(0, startIdx).trimEnd();
|
||||
const afterSection = content.substring(endIdx + endMarker.length).trimStart();
|
||||
|
||||
const cleanedContent = beforeSection + (afterSection ? '\n\n' + afterSection : '');
|
||||
|
||||
try {
|
||||
fs.writeFileSync(this.claudeMdPath, cleanedContent, 'utf-8');
|
||||
|
||||
const originalSize = content.length;
|
||||
const newSize = cleanedContent.length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
backup: backup.path,
|
||||
originalSize,
|
||||
newSize,
|
||||
removed: originalSize - newSize
|
||||
};
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
this.rollback(backup.path);
|
||||
return {
|
||||
success: false,
|
||||
reason: `Write failed: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeMdValidator;
|
||||
Reference in New Issue
Block a user