Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:04 +08:00
commit e758c0ab84
56 changed files with 9997 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* Project Type Detector
*
* Detects project type and test framework to provide appropriate TDD configuration.
*/
const fs = require('fs');
const path = require('path');
class ProjectDetector {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.packageJsonPath = path.join(projectRoot, 'package.json');
}
/**
* Detect project configuration
* @returns {object} Project details
*/
detect() {
const result = {
type: 'unknown',
testFramework: 'unknown',
hasPackageJson: false,
hasGit: false,
hasTypeScript: false,
projectName: path.basename(this.projectRoot),
recommendations: []
};
// Check for package.json
if (fs.existsSync(this.packageJsonPath)) {
result.hasPackageJson = true;
this.analyzePackageJson(result);
}
// Check for git
if (fs.existsSync(path.join(this.projectRoot, '.git'))) {
result.hasGit = true;
} else {
result.recommendations.push('Initialize git repository for version control');
}
// Check for TypeScript
if (fs.existsSync(path.join(this.projectRoot, 'tsconfig.json'))) {
result.hasTypeScript = true;
}
// Detect project type
result.type = this.detectProjectType(result);
// Add recommendations
if (result.testFramework === 'unknown') {
result.recommendations.push('Install a test framework (vitest recommended)');
}
return result;
}
/**
* Analyze package.json for dependencies and scripts
* @param {object} result - Result object to populate
*/
analyzePackageJson(result) {
try {
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
result.projectName = packageJson.name || result.projectName;
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
// Detect test framework
if (allDeps['vitest']) {
result.testFramework = 'vitest';
} else if (allDeps['jest']) {
result.testFramework = 'jest';
} else if (allDeps['mocha']) {
result.testFramework = 'mocha';
} else if (allDeps['ava']) {
result.testFramework = 'ava';
}
// Detect framework/type
if (allDeps['react']) {
result.framework = 'react';
} else if (allDeps['vue']) {
result.framework = 'vue';
} else if (allDeps['@angular/core']) {
result.framework = 'angular';
} else if (allDeps['express']) {
result.framework = 'express';
} else if (allDeps['fastify']) {
result.framework = 'fastify';
} else if (allDeps['next']) {
result.framework = 'next';
}
// Check for existing test scripts
result.hasTestScript = packageJson.scripts && packageJson.scripts.test;
} catch (error) {
console.error('Error parsing package.json:', error.message);
}
}
/**
* Determine primary project type
* @param {object} result - Detection results
* @returns {string} Project type
*/
detectProjectType(result) {
if (result.framework === 'react') return 'react';
if (result.framework === 'vue') return 'vue';
if (result.framework === 'angular') return 'angular';
if (result.framework === 'next') return 'nextjs';
if (result.framework === 'express' || result.framework === 'fastify') return 'nodejs-backend';
if (result.hasTypeScript) return 'typescript';
if (result.hasPackageJson) return 'nodejs';
return 'unknown';
}
/**
* Get recommended test command for project
* @returns {string} Test command
*/
getTestCommand() {
const result = this.detect();
switch (result.testFramework) {
case 'vitest':
return 'vitest --run';
case 'jest':
return 'jest';
case 'mocha':
return 'mocha';
case 'ava':
return 'ava';
default:
return 'npm test';
}
}
/**
* Get recommended test file extension
* @returns {string} File extension
*/
getTestExtension() {
const result = this.detect();
if (result.hasTypeScript) {
return '.test.ts';
}
return '.test.js';
}
}
module.exports = ProjectDetector;

View File

@@ -0,0 +1,305 @@
#!/usr/bin/env node
/**
* Hook Installer
*
* Installs git hooks and Claude hooks for TDD enforcement
*/
const fs = require('fs');
const path = require('path');
class HookInstaller {
constructor(projectRoot, skillRoot) {
this.projectRoot = projectRoot;
this.skillRoot = skillRoot;
this.gitHooksDir = path.join(projectRoot, '.git', 'hooks');
this.claudeHooksDir = path.join(projectRoot, '.claude', 'hooks');
this.tddAutomationDir = path.join(projectRoot, '.tdd-automation');
}
/**
* Install all TDD hooks
* @returns {object} Installation result
*/
installAll() {
const results = {
success: true,
installed: [],
failed: [],
skipped: []
};
// Create directories
this.ensureDirectories();
// Copy TDD automation files
this.copyTddAutomationFiles(results);
// Install git pre-commit hook
this.installGitPreCommit(results);
// Install Claude hooks (if Claude hooks directory exists)
if (fs.existsSync(path.dirname(this.claudeHooksDir))) {
this.installClaudeHooks(results);
}
results.success = results.failed.length === 0;
return results;
}
/**
* Ensure required directories exist
*/
ensureDirectories() {
const dirs = [
this.tddAutomationDir,
path.join(this.tddAutomationDir, 'scripts'),
path.join(this.tddAutomationDir, 'templates'),
path.join(this.tddAutomationDir, 'hooks')
];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// Create Claude hooks directory if .claude exists
if (fs.existsSync(path.join(this.projectRoot, '.claude'))) {
if (!fs.existsSync(this.claudeHooksDir)) {
fs.mkdirSync(this.claudeHooksDir, { recursive: true });
}
}
}
/**
* Copy TDD automation files to project
* @param {object} results - Results object to update
*/
copyTddAutomationFiles(results) {
const scriptsDir = path.join(this.skillRoot, 'scripts');
const templatesDir = path.join(this.skillRoot, 'templates');
const targetScriptsDir = path.join(this.tddAutomationDir, 'scripts');
const targetTemplatesDir = path.join(this.tddAutomationDir, 'templates');
try {
// Copy scripts
if (fs.existsSync(scriptsDir)) {
this.copyDirectory(scriptsDir, targetScriptsDir);
results.installed.push('TDD automation scripts');
}
// Copy templates
if (fs.existsSync(templatesDir)) {
this.copyDirectory(templatesDir, targetTemplatesDir);
results.installed.push('TDD templates');
}
} catch (error) {
results.failed.push(`Copy TDD files: ${error.message}`);
}
}
/**
* Install git pre-commit hook
* @param {object} results - Results object to update
*/
installGitPreCommit(results) {
if (!fs.existsSync(this.gitHooksDir)) {
results.skipped.push('Git pre-commit hook (no .git directory)');
return;
}
const hookPath = path.join(this.gitHooksDir, 'pre-commit');
const templatePath = path.join(this.skillRoot, 'templates', 'pre-commit.sh');
try {
// Check if hook already exists
if (fs.existsSync(hookPath)) {
const existing = fs.readFileSync(hookPath, 'utf-8');
// Check if our TDD hook is already installed
if (existing.includes('TDD_AUTOMATION')) {
results.skipped.push('Git pre-commit hook (already installed)');
return;
}
// Backup existing hook
const backupPath = hookPath + '.backup';
fs.copyFileSync(hookPath, backupPath);
results.installed.push(`Git pre-commit hook backup (${backupPath})`);
// Append our hook to existing
const tddHook = fs.readFileSync(templatePath, 'utf-8');
fs.appendFileSync(hookPath, '\n\n' + tddHook);
results.installed.push('Git pre-commit hook (appended)');
} else {
// Install new hook
fs.copyFileSync(templatePath, hookPath);
fs.chmodSync(hookPath, '755');
results.installed.push('Git pre-commit hook (new)');
}
} catch (error) {
results.failed.push(`Git pre-commit hook: ${error.message}`);
}
}
/**
* Install Claude hooks
* @param {object} results - Results object to update
*/
installClaudeHooks(results) {
const hooksToInstall = [
{
name: 'tdd-auto-enforcer.sh',
description: 'TDD auto-enforcer hook'
}
];
for (const hook of hooksToInstall) {
const sourcePath = path.join(this.skillRoot, 'templates', hook.name);
const targetPath = path.join(this.claudeHooksDir, hook.name);
try {
if (fs.existsSync(targetPath)) {
results.skipped.push(`${hook.description} (already exists)`);
continue;
}
if (!fs.existsSync(sourcePath)) {
results.failed.push(`${hook.description} (template not found)`);
continue;
}
fs.copyFileSync(sourcePath, targetPath);
fs.chmodSync(targetPath, '755');
results.installed.push(hook.description);
} catch (error) {
results.failed.push(`${hook.description}: ${error.message}`);
}
}
}
/**
* Copy directory recursively
* @param {string} src - Source directory
* @param {string} dest - Destination directory
*/
copyDirectory(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
this.copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
// Make scripts executable
if (entry.name.endsWith('.sh') || entry.name.endsWith('.js')) {
try {
fs.chmodSync(destPath, '755');
} catch (error) {
// Ignore chmod errors on systems that don't support it
}
}
}
}
}
/**
* Uninstall all TDD hooks
* @returns {object} Uninstallation result
*/
uninstallAll() {
const results = {
success: true,
removed: [],
failed: []
};
// Remove git pre-commit hook (only our section)
this.uninstallGitPreCommit(results);
// Remove Claude hooks
this.uninstallClaudeHooks(results);
// Remove TDD automation directory
if (fs.existsSync(this.tddAutomationDir)) {
try {
fs.rmSync(this.tddAutomationDir, { recursive: true, force: true });
results.removed.push('.tdd-automation directory');
} catch (error) {
results.failed.push(`.tdd-automation directory: ${error.message}`);
}
}
results.success = results.failed.length === 0;
return results;
}
/**
* Uninstall git pre-commit hook
* @param {object} results - Results object to update
*/
uninstallGitPreCommit(results) {
const hookPath = path.join(this.gitHooksDir, 'pre-commit');
if (!fs.existsSync(hookPath)) {
return;
}
try {
const content = fs.readFileSync(hookPath, 'utf-8');
// Check if our hook is installed
if (!content.includes('TDD_AUTOMATION')) {
return;
}
// If the entire file is our hook, remove it
if (content.trim().startsWith('#!/bin/bash') && content.includes('TDD_AUTOMATION')) {
// Check if there's a backup
const backupPath = hookPath + '.backup';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, hookPath);
results.removed.push('Git pre-commit hook (restored from backup)');
} else {
fs.unlinkSync(hookPath);
results.removed.push('Git pre-commit hook (removed)');
}
}
} catch (error) {
results.failed.push(`Git pre-commit hook: ${error.message}`);
}
}
/**
* Uninstall Claude hooks
* @param {object} results - Results object to update
*/
uninstallClaudeHooks(results) {
const hooksToRemove = ['tdd-auto-enforcer.sh'];
for (const hookName of hooksToRemove) {
const hookPath = path.join(this.claudeHooksDir, hookName);
if (fs.existsSync(hookPath)) {
try {
fs.unlinkSync(hookPath);
results.removed.push(`Claude hook: ${hookName}`);
} catch (error) {
results.failed.push(`Claude hook ${hookName}: ${error.message}`);
}
}
}
}
}
module.exports = HookInstaller;

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env node
/**
* CLAUDE.md Merger
*
* Safely merges TDD automation configuration into existing CLAUDE.md files
* while preserving all existing content.
*/
const fs = require('fs');
const path = require('path');
class ClaudeMdMerger {
constructor(existingContent = '') {
this.existingContent = existingContent;
this.version = '0.2.0';
}
/**
* Merge TDD section with existing content
* @returns {string} Merged CLAUDE.md content
*/
merge() {
// Strategy: Append TDD section with clear delimiters
// This preserves ALL existing content
const separator = '\n\n' + '='.repeat(80) + '\n\n';
const timestamp = new Date().toISOString();
const mergedContent = [
this.existingContent.trimEnd(),
separator,
'<!-- TDD_AUTOMATION_START -->',
`<!-- Added by tdd-automation skill -->`,
`<!-- Version: ${this.version} -->`,
`<!-- Date: ${timestamp} -->`,
'',
this.buildTddSection(),
'',
'<!-- TDD_AUTOMATION_END -->',
].join('\n');
return mergedContent;
}
/**
* Build the complete TDD automation section
* @returns {string} TDD section content
*/
buildTddSection() {
return `# TDD Red-Green-Refactor Automation (Auto-Installed)
## ⚠️ CRITICAL: TDD is MANDATORY for all feature development
\`\`\`yaml
tdd-automation-version: ${this.version}
tdd-enforcement-level: strict
tdd-phase-tracking: required
\`\`\`
## Development Workflow (STRICTLY ENFORCED)
When implementing ANY new feature or functionality, you MUST follow this sequence:
### Phase 1: RED (Write Failing Test First)
**ALWAYS START HERE. DO NOT SKIP.**
1. **Create or modify the test file BEFORE writing ANY implementation code**
2. Write a test that describes the expected behavior
3. Run the test: \`npm run test:tdd -- <test-file>\`
4. **VERIFY the test fails for the RIGHT reason** (not syntax error, but missing functionality)
5. Use TodoWrite to mark RED phase complete
**DO NOT proceed to implementation until test fails correctly.**
#### RED Phase Checklist:
- [ ] Test file created/modified
- [ ] Test describes expected behavior clearly
- [ ] Test executed and verified to fail
- [ ] Failure reason is correct (missing functionality, not syntax error)
- [ ] RED phase marked in TodoWrite
### Phase 2: GREEN (Minimal Implementation)
**Only after RED phase is verified:**
1. Write ONLY enough code to make the failing test pass
2. No extra features, no "while we're here" additions
3. Keep it simple and focused on passing the test
4. Run test: \`npm run test:tdd -- <test-file>\`
5. **VERIFY the test now passes**
6. Use TodoWrite to mark GREEN phase complete
#### GREEN Phase Checklist:
- [ ] Minimal code written (no extras)
- [ ] Test executed and verified to pass
- [ ] All existing tests still pass
- [ ] GREEN phase marked in TodoWrite
### Phase 3: REFACTOR (Improve Quality)
**Only after GREEN phase is verified:**
1. Improve code structure, naming, and quality
2. Extract duplicated code
3. Simplify complex logic
4. Run full test suite: \`npm run test:tdd\`
5. **VERIFY all tests still pass after refactoring**
6. Use TodoWrite to mark REFACTOR phase complete
#### REFACTOR Phase Checklist:
- [ ] Code structure improved
- [ ] Duplicated code extracted
- [ ] Complex logic simplified
- [ ] All tests still pass
- [ ] REFACTOR phase marked in TodoWrite
## Pre-Implementation TodoWrite Template (ALWAYS Use)
Before writing ANY implementation code, create a todo list with these phases:
\`\`\`markdown
[ ] RED: Write failing test for [feature name]
[ ] Verify test fails with expected error message
[ ] GREEN: Implement minimal code to pass test
[ ] Verify test passes
[ ] REFACTOR: Improve code quality (if needed)
[ ] Verify all tests still pass
\`\`\`
**Example:**
\`\`\`markdown
[ ] RED: Write failing test for user authentication
[ ] Verify test fails with "authenticateUser is not defined"
[ ] GREEN: Implement minimal authenticateUser function
[ ] Verify test passes
[ ] REFACTOR: Extract validation logic to separate function
[ ] Verify all tests still pass
\`\`\`
## Critical Rules (NEVER Violate)
- ❌ **NEVER** write implementation code before writing the test
- ❌ **NEVER** skip the RED phase verification
- ❌ **NEVER** skip the GREEN phase verification
- ❌ **NEVER** skip TodoWrite phase tracking
- ✅ **ALWAYS** run tests to verify RED and GREEN states
- ✅ **ALWAYS** use TodoWrite to track TDD phases
- ✅ **ALWAYS** create test files BEFORE implementation files
- ✅ **ALWAYS** commit tests BEFORE committing implementation
- ✅ **ALWAYS** use semantic test names: \`"should [behavior] when [condition]"\`
## Test Execution Commands
This project has been configured with TDD-optimized test scripts:
\`\`\`bash
# Run all tests once (non-watch mode)
npm run test:tdd
# Run tests in watch mode for development
npm run test:tdd:watch
# Run specific test file (RED/GREEN phase)
npm run test:tdd -- path/to/test.test.ts
# Validate TDD compliance
npm run validate:tdd
\`\`\`
## File Structure Convention
For every implementation file, there must be a corresponding test file:
\`\`\`
src/features/auth/login.ts → Implementation
src/features/auth/login.test.ts → Tests (created FIRST)
src/utils/validation.ts → Implementation
src/utils/validation.test.ts → Tests (created FIRST)
\`\`\`
## Test Naming Convention
Use the pattern: \`"should [behavior] when [condition]"\`
**Good Examples:**
- \`"should return true when email is valid"\`
- \`"should throw error when password is too short"\`
- \`"should update user profile when data is valid"\`
**Bad Examples:**
- \`"test email validation"\` (not descriptive)
- \`"it works"\` (not specific)
- \`"validates email"\` (missing condition)
## Violation Handling
If you accidentally start writing implementation before tests:
1. **STOP immediately**
2. Create the test file first
3. Write the failing test
4. Verify RED state
5. **THEN** proceed with implementation
## TDD Automation Features
This installation includes:
- ✅ **Pre-commit hooks**: Validate tests exist before committing implementation
- ✅ **Test scaffolding**: Generate test file templates with \`npm run generate:test <file>\`
- ✅ **TDD validation**: Check compliance with \`npm run validate:tdd\`
- ✅ **Rollback capability**: Restore previous CLAUDE.md if needed
## Help & Maintenance
### Check TDD Compliance
\`\`\`bash
npm run validate:tdd
\`\`\`
### Generate Test Template
\`\`\`bash
npm run generate:test src/features/auth/login.ts
\`\`\`
### Rollback This Automation
\`\`\`bash
node .tdd-automation/scripts/rollback-tdd.js
\`\`\`
### Remove TDD Section
\`\`\`bash
node .tdd-automation/scripts/remove-tdd-section.js
\`\`\`
### View Backups
\`\`\`bash
ls -lh .claude/CLAUDE.md.backup.*
\`\`\`
## Documentation
- Setup documentation: \`.tdd-automation/README.md\`
- Pre-commit hook: \`.git/hooks/pre-commit\`
- Test templates: \`.tdd-automation/templates/\`
## Success Metrics
Track these metrics to measure TDD adherence:
- Test-first adherence rate: >95%
- RED-GREEN-REFACTOR cycle completion: >90%
- Defect escape rate: <2%
- Test coverage: >80%
---
**Note:** This section was automatically added by the tdd-automation skill.
For support or to report issues, see: .tdd-automation/README.md`;
}
/**
* Create standalone TDD section (for new CLAUDE.md files)
* @returns {string} TDD section content without existing content
*/
static createNew() {
const merger = new ClaudeMdMerger('');
const timestamp = new Date().toISOString();
return [
'<!-- TDD_AUTOMATION_START -->',
`<!-- Added by tdd-automation skill -->`,
`<!-- Version: 0.2.0 -->`,
`<!-- Date: ${timestamp} -->`,
'',
merger.buildTddSection(),
'',
'<!-- TDD_AUTOMATION_END -->',
].join('\n');
}
}
module.exports = ClaudeMdMerger;

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env node
/**
* Package.json Updater
*
* Safely adds TDD-related npm scripts to package.json
*/
const fs = require('fs');
const path = require('path');
class PackageJsonUpdater {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.packageJsonPath = path.join(projectRoot, 'package.json');
}
/**
* Add TDD scripts to package.json
* @param {string} testCommand - Base test command (e.g., 'vitest --run')
* @returns {object} Update result
*/
addTddScripts(testCommand = 'vitest --run') {
if (!fs.existsSync(this.packageJsonPath)) {
return {
success: false,
reason: 'package.json not found'
};
}
try {
// Create backup
const backupPath = this.packageJsonPath + '.backup';
fs.copyFileSync(this.packageJsonPath, backupPath);
// Read and parse package.json
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
// Ensure scripts object exists
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Define TDD scripts
const tddScripts = {
'test:tdd': testCommand,
'test:tdd:watch': testCommand.replace('--run', '').trim(),
'test:red': `${testCommand} --reporter=verbose`,
'test:green': `${testCommand} --reporter=verbose`,
'validate:tdd': 'node .tdd-automation/scripts/validate-tdd.js',
'generate:test': 'node .tdd-automation/scripts/generate-test.js'
};
// Track what was added
const added = [];
const skipped = [];
// Add scripts that don't exist
for (const [key, value] of Object.entries(tddScripts)) {
if (!packageJson.scripts[key]) {
packageJson.scripts[key] = value;
added.push(key);
} else {
skipped.push(key);
}
}
// Write updated package.json with pretty formatting
fs.writeFileSync(
this.packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n',
'utf-8'
);
return {
success: true,
added,
skipped,
backup: backupPath
};
} catch (error) {
return {
success: false,
reason: error.message
};
}
}
/**
* Remove TDD scripts from package.json
* @returns {object} Removal result
*/
removeTddScripts() {
if (!fs.existsSync(this.packageJsonPath)) {
return {
success: false,
reason: 'package.json not found'
};
}
try {
// Create backup
const backupPath = this.packageJsonPath + '.backup';
fs.copyFileSync(this.packageJsonPath, backupPath);
// Read and parse package.json
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
if (!packageJson.scripts) {
return {
success: true,
removed: [],
backup: backupPath
};
}
// Define TDD script keys to remove
const tddScriptKeys = [
'test:tdd',
'test:tdd:watch',
'test:red',
'test:green',
'validate:tdd',
'generate:test'
];
// Track what was removed
const removed = [];
// Remove TDD scripts
for (const key of tddScriptKeys) {
if (packageJson.scripts[key]) {
delete packageJson.scripts[key];
removed.push(key);
}
}
// Write updated package.json
fs.writeFileSync(
this.packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n',
'utf-8'
);
return {
success: true,
removed,
backup: backupPath
};
} catch (error) {
return {
success: false,
reason: error.message
};
}
}
/**
* Check if TDD scripts are already installed
* @returns {boolean} True if TDD scripts exist
*/
hasTddScripts() {
if (!fs.existsSync(this.packageJsonPath)) {
return false;
}
try {
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
if (!packageJson.scripts) {
return false;
}
// Check if any TDD scripts exist
const tddScriptKeys = ['test:tdd', 'validate:tdd', 'generate:test'];
return tddScriptKeys.some(key => packageJson.scripts[key]);
} catch (error) {
return false;
}
}
}
module.exports = PackageJsonUpdater;

View 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;