Initial commit
This commit is contained in:
14
skills/tdd-automation/CHANGELOG.md
Normal file
14
skills/tdd-automation/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Refactored to Anthropic progressive disclosure pattern
|
||||
- Updated description with "Use PROACTIVELY when..." format
|
||||
- Removed version/author/category/tags from frontmatter
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release with RED-GREEN-REFACTOR cycle enforcement
|
||||
- Automated TDD workflow with git hooks and npm scripts
|
||||
- CPU usage prevention safeguards
|
||||
- Integration with Testing Trophy methodology
|
||||
340
skills/tdd-automation/README.md
Normal file
340
skills/tdd-automation/README.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# TDD Red-Green-Refactor Automation
|
||||
|
||||
Automated TDD enforcement for LLM-assisted development. This skill installs infrastructure that guides Claude Code to automatically follow TDD workflow without manual intervention.
|
||||
|
||||
## Version
|
||||
|
||||
**0.2.0** - Stable release with comprehensive safety features
|
||||
|
||||
## What This Skill Does
|
||||
|
||||
Transforms your project so that when you ask Claude to implement features, it **automatically**:
|
||||
|
||||
1. 🔴 **RED Phase** - Writes failing tests FIRST
|
||||
2. 🟢 **GREEN Phase** - Implements minimal code to pass tests
|
||||
3. 🔵 **REFACTOR Phase** - Improves code quality while keeping tests green
|
||||
|
||||
**No slash commands. No manual prompting. Just say "implement X" and TDD happens.**
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From claudex marketplace
|
||||
/plugin install tdd-automation
|
||||
|
||||
# Or directly invoke the skill
|
||||
tdd-automation
|
||||
```
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
### 1. CLAUDE.md Configuration
|
||||
- Adds TDD requirements to project's `.claude/CLAUDE.md`
|
||||
- **Safety**: Backs up existing files, merges cleanly, fully reversible
|
||||
- Configures LLM to automatically follow TDD workflow
|
||||
|
||||
### 2. npm Scripts
|
||||
```json
|
||||
{
|
||||
"test:tdd": "vitest --run",
|
||||
"test:tdd:watch": "vitest",
|
||||
"test:red": "vitest --run --reporter=verbose",
|
||||
"test:green": "vitest --run --reporter=verbose",
|
||||
"validate:tdd": "node .tdd-automation/scripts/validate-tdd.js",
|
||||
"generate:test": "node .tdd-automation/scripts/generate-test.js"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Git Hooks
|
||||
- **Pre-commit hook**: Validates tests exist before allowing commits
|
||||
- Prevents TDD violations at the version control level
|
||||
- Can be bypassed with `--no-verify` if needed
|
||||
|
||||
### 4. Helper Scripts
|
||||
```
|
||||
.tdd-automation/
|
||||
├── scripts/
|
||||
│ ├── generate-test.js # Create test templates
|
||||
│ ├── validate-tdd.js # Check compliance
|
||||
│ ├── rollback-tdd.js # Restore previous state
|
||||
│ └── remove-tdd-section.js # Clean uninstall
|
||||
└── templates/
|
||||
└── test-template.js # Test file template
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Before Installation
|
||||
```
|
||||
You: "Add user authentication"
|
||||
Claude: *writes implementation code directly* ❌
|
||||
```
|
||||
|
||||
### After Installation
|
||||
```
|
||||
You: "Add user authentication"
|
||||
|
||||
Claude: "I'll implement user authentication following TDD red-green-refactor.
|
||||
|
||||
Let me create a todo list to track the phases:
|
||||
- [ ] RED: Write failing test for user authentication
|
||||
- [ ] Verify test fails with expected error
|
||||
- [ ] GREEN: Implement minimal authentication logic
|
||||
- [ ] Verify test passes
|
||||
- [ ] REFACTOR: Improve code quality
|
||||
|
||||
Starting RED phase - I'll create the test file first..."
|
||||
|
||||
*Creates src/auth/authenticate.test.ts*
|
||||
*Writes failing test*
|
||||
*Runs: npm run test:red*
|
||||
*Verifies failure*
|
||||
|
||||
"Test is failing as expected. Moving to GREEN phase..."
|
||||
|
||||
*Implements minimal code*
|
||||
*Runs: npm run test:green*
|
||||
*Verifies pass*
|
||||
|
||||
"All tests passing. Moving to REFACTOR phase..."
|
||||
|
||||
*Improves code structure*
|
||||
*Runs: npm run test:tdd*
|
||||
|
||||
"✅ Feature complete with TDD workflow followed."
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Validation
|
||||
```bash
|
||||
# Check if project follows TDD practices
|
||||
npm run validate:tdd
|
||||
```
|
||||
|
||||
### Test Generation
|
||||
```bash
|
||||
# Generate test template for implementation file
|
||||
npm run generate:test src/features/auth/login.ts
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
```bash
|
||||
# Rollback to previous CLAUDE.md
|
||||
node .tdd-automation/scripts/rollback-tdd.js
|
||||
|
||||
# Remove only TDD section from CLAUDE.md
|
||||
node .tdd-automation/scripts/remove-tdd-section.js
|
||||
|
||||
# View available backups
|
||||
ls -lh .claude/CLAUDE.md.backup.*
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
|
||||
### ✅ Non-Destructive Installation
|
||||
- **Automatic backups** before any file modification
|
||||
- **Merge strategy** preserves all existing content
|
||||
- **Clear markers** for easy identification of additions
|
||||
- **Rollback capability** to restore previous state
|
||||
|
||||
### ✅ Validation Before Installation
|
||||
- Detects existing TDD automation (skips if present)
|
||||
- Checks for existing CLAUDE.md (merges safely)
|
||||
- Validates project structure
|
||||
- Reports warnings for missing dependencies
|
||||
|
||||
### ✅ Clean Uninstallation
|
||||
- Remove TDD section without affecting other content
|
||||
- Restore from timestamped backups
|
||||
- No orphaned files or configurations
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enforcement Level
|
||||
|
||||
By default, TDD is **strictly enforced**. To customize:
|
||||
|
||||
Edit `.claude/CLAUDE.md` and modify:
|
||||
```yaml
|
||||
tdd-enforcement-level: strict # or: advisory, optional
|
||||
```
|
||||
|
||||
- **strict**: LLM must follow TDD (default)
|
||||
- **advisory**: LLM suggests TDD but allows flexibility
|
||||
- **optional**: TDD is available but not required
|
||||
|
||||
### Test Framework
|
||||
|
||||
The skill auto-detects your test framework:
|
||||
- Vitest (default, recommended)
|
||||
- Jest
|
||||
- Mocha
|
||||
- AVA
|
||||
|
||||
To override, edit `.tdd-automation/config.json`:
|
||||
```json
|
||||
{
|
||||
"testCommand": "jest",
|
||||
"testExtension": ".test.ts"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: LLM not following TDD
|
||||
|
||||
**Solution:**
|
||||
1. Check CLAUDE.md has TDD configuration:
|
||||
```bash
|
||||
grep -A 5 "TDD_AUTOMATION" .claude/CLAUDE.md
|
||||
```
|
||||
|
||||
2. Verify npm scripts installed:
|
||||
```bash
|
||||
npm run test:tdd
|
||||
```
|
||||
|
||||
3. Validate installation:
|
||||
```bash
|
||||
npm run validate:tdd
|
||||
```
|
||||
|
||||
### Issue: Git hook blocking commits
|
||||
|
||||
**Solution:**
|
||||
1. Ensure tests exist for implementation files
|
||||
2. Commit tests before implementation:
|
||||
```bash
|
||||
git add src/**/*.test.ts
|
||||
git commit -m "Add tests for feature X"
|
||||
git add src/**/!(*test).ts
|
||||
git commit -m "Implement feature X"
|
||||
```
|
||||
|
||||
3. Or bypass hook temporarily:
|
||||
```bash
|
||||
git commit --no-verify
|
||||
```
|
||||
|
||||
### Issue: Want to remove TDD automation
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Option 1: Remove only CLAUDE.md section
|
||||
node .tdd-automation/scripts/remove-tdd-section.js
|
||||
|
||||
# Option 2: Rollback to previous CLAUDE.md
|
||||
node .tdd-automation/scripts/rollback-tdd.js
|
||||
|
||||
# Option 3: Manual removal
|
||||
# 1. Edit .claude/CLAUDE.md
|
||||
# 2. Delete section between <!-- TDD_AUTOMATION_START --> and <!-- TDD_AUTOMATION_END -->
|
||||
# 3. Remove .tdd-automation/ directory
|
||||
# 4. Remove npm scripts from package.json
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ User Request │
|
||||
│ "Implement feature X" │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CLAUDE.md (LLM Configuration) │
|
||||
│ • TDD workflow requirements │
|
||||
│ • Phase-by-phase instructions │
|
||||
│ • TodoWrite templates │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Claude Code (LLM) │
|
||||
│ 1. Creates test file │
|
||||
│ 2. Writes failing test (RED) │
|
||||
│ 3. Runs: npm run test:red │
|
||||
│ 4. Implements code (GREEN) │
|
||||
│ 5. Runs: npm run test:green │
|
||||
│ 6. Refactors (REFACTOR) │
|
||||
│ 7. Runs: npm run test:tdd │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Git Pre-Commit Hook │
|
||||
│ • Validates tests exist │
|
||||
│ • Blocks commits without tests │
|
||||
│ • Ensures test-first compliance │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── .claude/
|
||||
│ ├── CLAUDE.md # TDD configuration (modified)
|
||||
│ ├── CLAUDE.md.backup.* # Safety backups
|
||||
│ └── hooks/
|
||||
│ └── tdd-auto-enforcer.sh # Pre-prompt hook (optional)
|
||||
├── .git/
|
||||
│ └── hooks/
|
||||
│ └── pre-commit # TDD validation (modified)
|
||||
├── .tdd-automation/ # Installed by skill
|
||||
│ ├── scripts/
|
||||
│ │ ├── generate-test.js
|
||||
│ │ ├── validate-tdd.js
|
||||
│ │ ├── rollback-tdd.js
|
||||
│ │ └── remove-tdd-section.js
|
||||
│ ├── templates/
|
||||
│ │ └── test-template.js
|
||||
│ └── README.md
|
||||
└── package.json # npm scripts added
|
||||
```
|
||||
|
||||
## Success Metrics
|
||||
|
||||
Track TDD adherence:
|
||||
|
||||
```bash
|
||||
npm run validate:tdd
|
||||
```
|
||||
|
||||
**Target Metrics:**
|
||||
- Test-first adherence: >95%
|
||||
- RED-GREEN-REFACTOR cycles: >90%
|
||||
- Defect escape rate: <2%
|
||||
- Test coverage: >80%
|
||||
|
||||
## Contributing
|
||||
|
||||
This skill was auto-generated by the Pattern Suggestion Pipeline from 37 successful TDD applications with 86% success rate.
|
||||
|
||||
**Issues or improvements:**
|
||||
- GitHub: https://github.com/cskiro/claudex/issues
|
||||
- Pattern source: Issue #13
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Version History
|
||||
|
||||
- **0.2.0** (2025-11-02)
|
||||
- Stable release with full implementation
|
||||
- Comprehensive safety features (backup, rollback, merge)
|
||||
- Automatic LLM TDD enforcement via CLAUDE.md
|
||||
- Git hooks for pre-commit validation
|
||||
- Helper scripts (generate-test, validate-tdd)
|
||||
- Multi-framework support (Vitest, Jest, Mocha, AVA)
|
||||
- Complete documentation and troubleshooting guide
|
||||
|
||||
- **0.1.0** (2025-11-02)
|
||||
- Initial proof-of-concept
|
||||
- Auto-generated by Pattern Pipeline
|
||||
- Basic skill structure and manifest
|
||||
235
skills/tdd-automation/SKILL.md
Normal file
235
skills/tdd-automation/SKILL.md
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
name: tdd-automation
|
||||
description: Use PROACTIVELY when starting new projects requiring strict TDD adherence, or when user wants to enforce test-first workflow. Automates red-green-refactor cycle for LLM-assisted development with git hooks, npm scripts, and CLAUDE.md configuration. Not for prototypes or projects without test requirements.
|
||||
---
|
||||
|
||||
# TDD Red-Green-Refactor Automation
|
||||
|
||||
## Overview
|
||||
|
||||
This skill transforms any project to automatically enforce TDD (Test-Driven Development) workflow when using LLMs for code generation. Once installed, Claude Code will automatically follow the red-green-refactor cycle without manual prompting.
|
||||
|
||||
**Version:** 0.2.0
|
||||
**Status:** Stable
|
||||
**Success Rate:** 86% (based on 37 pattern applications)
|
||||
**Source:** Pattern Suggestion Pipeline (Issue #13)
|
||||
|
||||
## What This Skill Does
|
||||
|
||||
**Automatic TDD Enforcement** - When you ask Claude to implement a feature, it will:
|
||||
|
||||
1. 🔴 **RED Phase**: Write failing test FIRST
|
||||
2. 🟢 **GREEN Phase**: Implement minimal code to pass test
|
||||
3. 🔵 **REFACTOR Phase**: Improve code quality while keeping tests green
|
||||
|
||||
**No manual steps required** - The LLM is automatically configured through CLAUDE.md to follow this workflow.
|
||||
|
||||
## Installation Process
|
||||
|
||||
When you invoke this skill, it will:
|
||||
|
||||
1. **Detect project configuration**
|
||||
- Identify test framework (Vitest, Jest, Mocha, AVA)
|
||||
- Check for TypeScript/JavaScript
|
||||
- Validate git repository exists
|
||||
|
||||
2. **Configure CLAUDE.md safely**
|
||||
- Create automatic backup before any changes
|
||||
- Merge TDD requirements (preserves existing content)
|
||||
- Add clear section markers for identification
|
||||
|
||||
3. **Install npm scripts**
|
||||
- `test:tdd` - Run all tests once
|
||||
- `test:tdd:watch` - Run tests in watch mode
|
||||
- `test:red` - Verify test fails (RED phase)
|
||||
- `test:green` - Verify test passes (GREEN phase)
|
||||
- `validate:tdd` - Check TDD compliance
|
||||
- `generate:test` - Create test template
|
||||
|
||||
4. **Install git hooks**
|
||||
- Pre-commit hook validates tests exist
|
||||
- Prevents commits without tests
|
||||
- Enforces test-first workflow
|
||||
|
||||
5. **Create helper scripts**
|
||||
- Test template generator
|
||||
- TDD compliance validator
|
||||
- Rollback utility
|
||||
- Section removal utility
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**Use this skill when:**
|
||||
- Starting a new project that requires strict TDD adherence
|
||||
- Migrating existing project to TDD workflow
|
||||
- Training team members on TDD practices
|
||||
- Ensuring consistent quality in LLM-generated code
|
||||
- Working on mission-critical code that needs high test coverage
|
||||
|
||||
**Don't use this skill if:**
|
||||
- Project doesn't have or need tests
|
||||
- Working on prototypes or experiments
|
||||
- TDD workflow conflicts with existing practices
|
||||
|
||||
## Trigger Phrases
|
||||
|
||||
The skill can be invoked by:
|
||||
- Direct invocation: `tdd-automation`
|
||||
- Natural language: "Set up TDD automation"
|
||||
- Natural language: "Configure automatic TDD workflow"
|
||||
- Natural language: "Install TDD enforcement"
|
||||
|
||||
## Behavior After Installation
|
||||
|
||||
Once installed, when you say something like:
|
||||
```
|
||||
"Implement user authentication"
|
||||
```
|
||||
|
||||
Claude will **automatically**:
|
||||
```
|
||||
1. Create todo list with TDD phases:
|
||||
- [ ] RED: Write failing test for user authentication
|
||||
- [ ] Verify test fails with expected error
|
||||
- [ ] GREEN: Implement minimal authentication logic
|
||||
- [ ] Verify test passes
|
||||
- [ ] REFACTOR: Improve code quality
|
||||
- [ ] Verify all tests still pass
|
||||
|
||||
2. Create test file FIRST:
|
||||
src/auth/authenticate.test.ts
|
||||
|
||||
3. Write failing test with clear description
|
||||
|
||||
4. Run test and verify RED state:
|
||||
npm run test:red -- src/auth/authenticate.test.ts
|
||||
|
||||
5. Implement minimal code:
|
||||
src/auth/authenticate.ts
|
||||
|
||||
6. Run test and verify GREEN state:
|
||||
npm run test:green -- src/auth/authenticate.test.ts
|
||||
|
||||
7. Refactor if needed while keeping tests green
|
||||
|
||||
8. Final validation:
|
||||
npm run test:tdd
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
|
||||
### Non-Destructive Installation
|
||||
- Automatic backups before file modifications
|
||||
- Merge strategy preserves all existing CLAUDE.md content
|
||||
- Clear markers (`<!-- TDD_AUTOMATION_START/END -->`) for identification
|
||||
- Rollback capability to restore previous state
|
||||
|
||||
### Validation Before Installation
|
||||
- Detects if TDD automation already installed (skips if present)
|
||||
- Checks for existing CLAUDE.md (merges safely)
|
||||
- Validates project structure
|
||||
- Reports warnings for missing dependencies
|
||||
|
||||
### Clean Uninstallation
|
||||
- Remove only TDD section without affecting other content
|
||||
- Restore from timestamped backups
|
||||
- Utility scripts for easy maintenance
|
||||
|
||||
## Response Style
|
||||
|
||||
After installation, this skill guides Claude Code to:
|
||||
|
||||
- **Be explicit**: Always state which TDD phase is active
|
||||
- **Be thorough**: Verify each phase completion before proceeding
|
||||
- **Be traceable**: Use TodoWrite to track progress
|
||||
- **Be safe**: Run tests to confirm RED/GREEN states
|
||||
- **Be helpful**: Provide clear error messages if TDD is violated
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
### Modified Files
|
||||
- `.claude/CLAUDE.md` - TDD workflow configuration added (backed up)
|
||||
- `package.json` - npm scripts added (backed up)
|
||||
- `.git/hooks/pre-commit` - TDD validation added (backed up if exists)
|
||||
|
||||
### Created Files
|
||||
```
|
||||
.tdd-automation/
|
||||
├── scripts/
|
||||
│ ├── generate-test.js # Test template generator
|
||||
│ ├── validate-tdd.js # Compliance checker
|
||||
│ ├── rollback-tdd.js # Restore previous state
|
||||
│ └── remove-tdd-section.js # Clean uninstall
|
||||
├── templates/
|
||||
│ └── test-template.js # Test file template
|
||||
└── README.md # Usage documentation
|
||||
|
||||
.claude/
|
||||
├── CLAUDE.md.backup.* # Timestamped backups
|
||||
└── hooks/
|
||||
└── tdd-auto-enforcer.sh # Pre-prompt hook (optional)
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Installation succeeds when:
|
||||
1. ✅ CLAUDE.md contains TDD configuration
|
||||
2. ✅ npm scripts added to package.json
|
||||
3. ✅ Git pre-commit hook installed (if git repo exists)
|
||||
4. ✅ Helper scripts created in .tdd-automation/
|
||||
5. ✅ Backups created for all modified files
|
||||
6. ✅ Validation passes: `npm run validate:tdd`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: LLM not following TDD
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
# Verify CLAUDE.md has TDD configuration
|
||||
grep "TDD_AUTOMATION" .claude/CLAUDE.md
|
||||
|
||||
# Verify npm scripts
|
||||
npm run test:tdd
|
||||
|
||||
# Run validation
|
||||
npm run validate:tdd
|
||||
```
|
||||
|
||||
### Issue: Git hook blocking commits
|
||||
|
||||
**Solution:**
|
||||
1. Ensure tests exist for implementation files
|
||||
2. Commit tests before implementation
|
||||
3. Or bypass temporarily: `git commit --no-verify`
|
||||
|
||||
### Issue: Want to remove TDD automation
|
||||
|
||||
**Options:**
|
||||
```bash
|
||||
# Option 1: Remove CLAUDE.md section only
|
||||
node .tdd-automation/scripts/remove-tdd-section.js
|
||||
|
||||
# Option 2: Rollback to previous CLAUDE.md
|
||||
node .tdd-automation/scripts/rollback-tdd.js
|
||||
```
|
||||
|
||||
## Generated By
|
||||
|
||||
🤖 **Pattern Suggestion Pipeline**
|
||||
- Detected from 37 successful TDD applications
|
||||
- Success rate: 86%
|
||||
- Source: GitHub Issue #13
|
||||
- Generated: 2025-11-02
|
||||
- Enhanced with full implementation: 2025-11-02
|
||||
|
||||
## Version History
|
||||
|
||||
- **0.2.0** (2025-11-02) - Stable release with full implementation
|
||||
- **0.1.0** (2025-11-02) - Initial proof-of-concept
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Full documentation: `README.md`
|
||||
- Helper scripts: `.tdd-automation/scripts/`
|
||||
- Examples: See README.md "Usage Examples" section
|
||||
0
skills/tdd-automation/examples/.gitkeep
Normal file
0
skills/tdd-automation/examples/.gitkeep
Normal file
417
skills/tdd-automation/index.js
Executable file
417
skills/tdd-automation/index.js
Executable file
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Red-Green-Refactor Automation Skill
|
||||
*
|
||||
* Main orchestrator that installs TDD automation infrastructure in a project.
|
||||
* Configures CLAUDE.md, git hooks, npm scripts, and helper utilities to enforce
|
||||
* TDD workflow automatically for LLM-assisted development.
|
||||
*
|
||||
* @author Pattern Suggestion Pipeline
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import utilities
|
||||
const ClaudeMdValidator = require('./utils/validate-claude-md');
|
||||
const ClaudeMdMerger = require('./utils/merge-claude-md');
|
||||
const ProjectDetector = require('./utils/detect-project-type');
|
||||
const PackageJsonUpdater = require('./utils/update-package-json');
|
||||
const HookInstaller = require('./utils/install-hooks');
|
||||
|
||||
class TddAutomationSkill {
|
||||
constructor() {
|
||||
this.projectRoot = process.cwd();
|
||||
this.skillRoot = __dirname;
|
||||
this.version = '0.2.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main installation workflow
|
||||
*/
|
||||
async install() {
|
||||
console.log('╔════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ TDD Red-Green-Refactor Automation ║');
|
||||
console.log('║ Version 0.2.0 ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const results = {
|
||||
success: false,
|
||||
steps: [],
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Detect project type
|
||||
console.log('🔍 Step 1: Detecting project configuration...\n');
|
||||
const projectInfo = await this.detectProject(results);
|
||||
|
||||
// Step 2: Validate and backup CLAUDE.md
|
||||
console.log('\n🔍 Step 2: Validating CLAUDE.md...\n');
|
||||
const validation = await this.validateClaudeMd(results);
|
||||
|
||||
if (validation.strategy === 'skip') {
|
||||
console.log('✅ TDD automation already installed');
|
||||
console.log(' No changes needed.\n');
|
||||
return results;
|
||||
}
|
||||
|
||||
// Step 3: Install/merge CLAUDE.md configuration
|
||||
console.log('\n📝 Step 3: Configuring CLAUDE.md...\n');
|
||||
await this.configureClaudeMd(validation, results);
|
||||
|
||||
// Step 4: Update package.json with TDD scripts
|
||||
console.log('\n📦 Step 4: Adding npm scripts...\n');
|
||||
await this.updatePackageJson(projectInfo, results);
|
||||
|
||||
// Step 5: Install hooks
|
||||
console.log('\n🪝 Step 5: Installing hooks...\n');
|
||||
await this.installHooks(results);
|
||||
|
||||
// Step 6: Verify installation
|
||||
console.log('\n✅ Step 6: Verifying installation...\n');
|
||||
await this.verifyInstallation(results);
|
||||
|
||||
// Success summary
|
||||
results.success = results.errors.length === 0;
|
||||
this.printSummary(results, projectInfo);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Installation failed:', error.message);
|
||||
console.error('\n Stack trace:', error.stack);
|
||||
results.errors.push(error.message);
|
||||
results.success = false;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect project type and configuration
|
||||
*/
|
||||
async detectProject(results) {
|
||||
const detector = new ProjectDetector(this.projectRoot);
|
||||
const projectInfo = detector.detect();
|
||||
|
||||
console.log(` Project: ${projectInfo.projectName}`);
|
||||
console.log(` Type: ${projectInfo.type}`);
|
||||
|
||||
if (projectInfo.testFramework !== 'unknown') {
|
||||
console.log(` Test Framework: ${projectInfo.testFramework}`);
|
||||
}
|
||||
|
||||
if (projectInfo.hasTypeScript) {
|
||||
console.log(' Language: TypeScript');
|
||||
}
|
||||
|
||||
if (projectInfo.hasGit) {
|
||||
console.log(' ✓ Git repository detected');
|
||||
} else {
|
||||
results.warnings.push('No git repository - git hooks will not be installed');
|
||||
}
|
||||
|
||||
if (projectInfo.recommendations.length > 0) {
|
||||
console.log('\n Recommendations:');
|
||||
projectInfo.recommendations.forEach(rec => {
|
||||
console.log(` • ${rec}`);
|
||||
results.warnings.push(rec);
|
||||
});
|
||||
}
|
||||
|
||||
results.steps.push('Project detection completed');
|
||||
return projectInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CLAUDE.md state
|
||||
*/
|
||||
async validateClaudeMd(results) {
|
||||
const validator = new ClaudeMdValidator(this.projectRoot);
|
||||
const validation = validator.validate();
|
||||
|
||||
console.log(` Location: ${validation.path}`);
|
||||
console.log(` Exists: ${validation.exists ? 'Yes' : 'No'}`);
|
||||
|
||||
if (validation.exists) {
|
||||
console.log(` Size: ${this.formatBytes(validation.size)}`);
|
||||
console.log(` Has Content: ${validation.hasExistingContent ? 'Yes' : 'No'}`);
|
||||
}
|
||||
|
||||
console.log(` Strategy: ${validation.strategy.toUpperCase()}`);
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
console.log('\n Notes:');
|
||||
validation.warnings.forEach(warning => {
|
||||
console.log(` • ${warning}`);
|
||||
});
|
||||
}
|
||||
|
||||
results.steps.push(`CLAUDE.md validation: ${validation.strategy}`);
|
||||
return validation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure CLAUDE.md with TDD automation
|
||||
*/
|
||||
async configureClaudeMd(validation, results) {
|
||||
const validator = new ClaudeMdValidator(this.projectRoot);
|
||||
|
||||
if (validation.strategy === 'merge') {
|
||||
// Existing content - merge safely
|
||||
console.log(' Creating backup...');
|
||||
const backup = validator.createBackup();
|
||||
|
||||
if (backup.success) {
|
||||
console.log(` ✓ Backup created: ${path.basename(backup.path)}`);
|
||||
results.steps.push(`Backup created: ${backup.path}`);
|
||||
} else {
|
||||
throw new Error(`Backup failed: ${backup.reason}`);
|
||||
}
|
||||
|
||||
// Read existing content
|
||||
const existingContent = fs.readFileSync(validator.claudeMdPath, 'utf-8');
|
||||
|
||||
// Merge with TDD section
|
||||
console.log(' Merging TDD configuration...');
|
||||
const merger = new ClaudeMdMerger(existingContent);
|
||||
const mergedContent = merger.merge();
|
||||
|
||||
// Write merged content
|
||||
fs.writeFileSync(validator.claudeMdPath, mergedContent, 'utf-8');
|
||||
|
||||
const newSize = mergedContent.length;
|
||||
const added = newSize - validation.size;
|
||||
|
||||
console.log(` ✓ CLAUDE.md updated`);
|
||||
console.log(` Original: ${this.formatBytes(validation.size)}`);
|
||||
console.log(` New: ${this.formatBytes(newSize)} (+${this.formatBytes(added)})`);
|
||||
|
||||
results.steps.push('CLAUDE.md merged with TDD configuration');
|
||||
|
||||
} else {
|
||||
// No existing content or empty - create new
|
||||
console.log(' Creating new CLAUDE.md with TDD configuration...');
|
||||
|
||||
const claudeDir = path.join(this.projectRoot, '.claude');
|
||||
if (!fs.existsSync(claudeDir)) {
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
const content = ClaudeMdMerger.createNew();
|
||||
fs.writeFileSync(validator.claudeMdPath, content, 'utf-8');
|
||||
|
||||
console.log(` ✓ CLAUDE.md created`);
|
||||
console.log(` Size: ${this.formatBytes(content.length)}`);
|
||||
|
||||
results.steps.push('CLAUDE.md created with TDD configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update package.json with TDD scripts
|
||||
*/
|
||||
async updatePackageJson(projectInfo, results) {
|
||||
if (!projectInfo.hasPackageJson) {
|
||||
console.log(' ⚠️ No package.json found - skipping npm scripts');
|
||||
results.warnings.push('No package.json - npm scripts not added');
|
||||
return;
|
||||
}
|
||||
|
||||
const updater = new PackageJsonUpdater(this.projectRoot);
|
||||
|
||||
// Check if already installed
|
||||
if (updater.hasTddScripts()) {
|
||||
console.log(' ✓ TDD scripts already installed');
|
||||
results.steps.push('TDD npm scripts already present');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine test command based on project
|
||||
const testCommand = projectInfo.testFramework === 'vitest'
|
||||
? 'vitest --run'
|
||||
: projectInfo.testFramework === 'jest'
|
||||
? 'jest'
|
||||
: 'npm test';
|
||||
|
||||
const result = updater.addTddScripts(testCommand);
|
||||
|
||||
if (result.success) {
|
||||
console.log(' ✓ Added TDD scripts to package.json');
|
||||
|
||||
if (result.added.length > 0) {
|
||||
console.log('\n Scripts added:');
|
||||
result.added.forEach(script => {
|
||||
console.log(` • ${script}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log('\n Scripts skipped (already exist):');
|
||||
result.skipped.forEach(script => {
|
||||
console.log(` • ${script}`);
|
||||
});
|
||||
}
|
||||
|
||||
results.steps.push(`Added ${result.added.length} npm scripts`);
|
||||
} else {
|
||||
results.warnings.push(`Failed to update package.json: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install git and Claude hooks
|
||||
*/
|
||||
async installHooks(results) {
|
||||
const installer = new HookInstaller(this.projectRoot, this.skillRoot);
|
||||
const result = installer.installAll();
|
||||
|
||||
if (result.installed.length > 0) {
|
||||
console.log(' Installed:');
|
||||
result.installed.forEach(item => {
|
||||
console.log(` ✓ ${item}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log('\n Skipped:');
|
||||
result.skipped.forEach(item => {
|
||||
console.log(` • ${item}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
console.log('\n Failed:');
|
||||
result.failed.forEach(item => {
|
||||
console.log(` ✗ ${item}`);
|
||||
results.warnings.push(`Hook installation failed: ${item}`);
|
||||
});
|
||||
}
|
||||
|
||||
results.steps.push(`Installed ${result.installed.length} hooks`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify installation was successful
|
||||
*/
|
||||
async verifyInstallation(results) {
|
||||
const checks = [];
|
||||
|
||||
// Check CLAUDE.md
|
||||
const claudeMdPath = path.join(this.projectRoot, '.claude', 'CLAUDE.md');
|
||||
if (fs.existsSync(claudeMdPath)) {
|
||||
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
||||
if (content.includes('TDD_AUTOMATION')) {
|
||||
checks.push('✓ CLAUDE.md configured');
|
||||
} else {
|
||||
checks.push('✗ CLAUDE.md missing TDD configuration');
|
||||
}
|
||||
} else {
|
||||
checks.push('✗ CLAUDE.md not found');
|
||||
}
|
||||
|
||||
// Check .tdd-automation directory
|
||||
const tddDir = path.join(this.projectRoot, '.tdd-automation');
|
||||
if (fs.existsSync(tddDir)) {
|
||||
checks.push('✓ .tdd-automation directory created');
|
||||
}
|
||||
|
||||
// Check package.json scripts
|
||||
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
if (packageJson.scripts && packageJson.scripts['test:tdd']) {
|
||||
checks.push('✓ npm scripts added');
|
||||
}
|
||||
}
|
||||
|
||||
checks.forEach(check => console.log(` ${check}`));
|
||||
results.steps.push('Installation verified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print installation summary
|
||||
*/
|
||||
printSummary(results, projectInfo) {
|
||||
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Installation Summary ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
if (results.success) {
|
||||
console.log('✅ TDD automation installed successfully!\n');
|
||||
} else {
|
||||
console.log('⚠️ TDD automation installed with warnings\n');
|
||||
}
|
||||
|
||||
console.log('📦 What was installed:\n');
|
||||
console.log(' • CLAUDE.md: TDD workflow configuration for LLM');
|
||||
console.log(' • npm scripts: test:tdd, validate:tdd, generate:test');
|
||||
console.log(' • Git hooks: pre-commit TDD validation');
|
||||
console.log(' • Helper scripts: .tdd-automation/scripts/');
|
||||
console.log(' • Templates: Test file generators\n');
|
||||
|
||||
console.log('🚀 Next steps:\n');
|
||||
console.log(' 1. Test the automation:');
|
||||
console.log(' Say: "Implement user authentication"\n');
|
||||
console.log(' 2. The LLM will automatically:');
|
||||
console.log(' • Follow TDD red-green-refactor workflow');
|
||||
console.log(' • Create tests BEFORE implementation');
|
||||
console.log(' • Run tests to verify each phase');
|
||||
console.log(' • Use TodoWrite to track progress\n');
|
||||
|
||||
console.log('📚 Available commands:\n');
|
||||
console.log(' npm run test:tdd # Run all tests');
|
||||
console.log(' npm run validate:tdd # Check TDD compliance');
|
||||
console.log(' npm run generate:test <file> # Create test template\n');
|
||||
|
||||
console.log('🔧 Maintenance:\n');
|
||||
console.log(' node .tdd-automation/scripts/rollback-tdd.js');
|
||||
console.log(' node .tdd-automation/scripts/remove-tdd-section.js\n');
|
||||
|
||||
if (results.warnings.length > 0) {
|
||||
console.log('⚠️ Warnings:\n');
|
||||
results.warnings.forEach(warning => {
|
||||
console.log(` • ${warning}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes for display
|
||||
*/
|
||||
formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main() {
|
||||
const skill = new TddAutomationSkill();
|
||||
const results = await skill.install();
|
||||
|
||||
if (!results.success && results.errors.length > 0) {
|
||||
console.error('Installation completed with errors');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TddAutomationSkill;
|
||||
0
skills/tdd-automation/reference/.gitkeep
Normal file
0
skills/tdd-automation/reference/.gitkeep
Normal file
198
skills/tdd-automation/scripts/generate-test.js
Executable file
198
skills/tdd-automation/scripts/generate-test.js
Executable file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Template Generator
|
||||
*
|
||||
* Generates test file templates based on implementation files.
|
||||
* Analyzes project structure and test framework to create appropriate templates.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function generateTest() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npm run generate:test <implementation-file>');
|
||||
console.error('');
|
||||
console.error('Example:');
|
||||
console.error(' npm run generate:test src/features/auth/login.ts');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const implFile = args[0];
|
||||
|
||||
if (!fs.existsSync(implFile)) {
|
||||
console.error(`❌ Error: File not found: ${implFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Determine test file path
|
||||
const ext = path.extname(implFile);
|
||||
const testFile = implFile.replace(ext, `.test${ext}`);
|
||||
|
||||
if (fs.existsSync(testFile)) {
|
||||
console.error(`❌ Error: Test file already exists: ${testFile}`);
|
||||
console.error(' Use a different name or edit the existing file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get implementation file info
|
||||
const filename = path.basename(implFile, ext);
|
||||
const implContent = fs.readFileSync(implFile, 'utf-8');
|
||||
|
||||
// Detect test framework
|
||||
const testFramework = detectTestFramework();
|
||||
|
||||
// Generate appropriate template
|
||||
const template = generateTemplate(filename, implFile, implContent, testFramework);
|
||||
|
||||
// Write test file
|
||||
fs.writeFileSync(testFile, template, 'utf-8');
|
||||
|
||||
console.log('✅ Test file created:', testFile);
|
||||
console.log('');
|
||||
console.log('📝 Next steps:');
|
||||
console.log(' 1. Edit the test file to add specific test cases');
|
||||
console.log(' 2. Run tests to verify RED phase:');
|
||||
console.log(` npm run test:red -- ${testFile}`);
|
||||
console.log(' 3. Implement the feature');
|
||||
console.log(' 4. Run tests to verify GREEN phase:');
|
||||
console.log(` npm run test:green -- ${testFile}`);
|
||||
}
|
||||
|
||||
function detectTestFramework() {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return 'vitest'; // default
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
if (allDeps['vitest']) return 'vitest';
|
||||
if (allDeps['jest']) return 'jest';
|
||||
if (allDeps['@jest/globals']) return 'jest';
|
||||
if (allDeps['mocha']) return 'mocha';
|
||||
|
||||
return 'vitest'; // default
|
||||
} catch (error) {
|
||||
return 'vitest';
|
||||
}
|
||||
}
|
||||
|
||||
function generateTemplate(filename, implFile, implContent, testFramework) {
|
||||
const relativePath = path.relative(path.dirname(implFile), implFile);
|
||||
const importPath = './' + path.basename(implFile, path.extname(implFile));
|
||||
|
||||
// Extract potential functions/classes to test
|
||||
const entities = extractEntities(implContent);
|
||||
|
||||
let template = '';
|
||||
|
||||
// Add imports based on test framework
|
||||
if (testFramework === 'vitest') {
|
||||
template += `import { describe, it, expect, beforeEach, afterEach } from 'vitest';\n`;
|
||||
} else if (testFramework === 'jest') {
|
||||
template += `import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';\n`;
|
||||
} else {
|
||||
template += `const { describe, it, expect, beforeEach, afterEach } = require('${testFramework}');\n`;
|
||||
}
|
||||
|
||||
// Add import for implementation
|
||||
template += `import * as impl from '${importPath}';\n`;
|
||||
template += `\n`;
|
||||
|
||||
// Add main describe block
|
||||
template += `describe('${filename}', () => {\n`;
|
||||
template += ` // TODO: Add setup and teardown if needed\n`;
|
||||
template += ` // beforeEach(() => {\n`;
|
||||
template += ` // // Setup before each test\n`;
|
||||
template += ` // });\n`;
|
||||
template += `\n`;
|
||||
template += ` // afterEach(() => {\n`;
|
||||
template += ` // // Cleanup after each test\n`;
|
||||
template += ` // });\n`;
|
||||
template += `\n`;
|
||||
|
||||
if (entities.length > 0) {
|
||||
// Generate test stubs for each entity
|
||||
entities.forEach(entity => {
|
||||
template += ` describe('${entity}', () => {\n`;
|
||||
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||
template += ` // Arrange\n`;
|
||||
template += ` // TODO: Set up test data and dependencies\n`;
|
||||
template += `\n`;
|
||||
template += ` // Act\n`;
|
||||
template += ` // TODO: Call the function/method being tested\n`;
|
||||
template += ` // const result = impl.${entity}();\n`;
|
||||
template += `\n`;
|
||||
template += ` // Assert\n`;
|
||||
template += ` // TODO: Verify the expected behavior\n`;
|
||||
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
template += ` // TODO: Add more test cases:\n`;
|
||||
template += ` // it('should handle edge case when [condition]', () => { ... });\n`;
|
||||
template += ` // it('should throw error when [invalid input]', () => { ... });\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
});
|
||||
} else {
|
||||
// Generic test template
|
||||
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||
template += ` // Arrange\n`;
|
||||
template += ` // TODO: Set up test data and dependencies\n`;
|
||||
template += `\n`;
|
||||
template += ` // Act\n`;
|
||||
template += ` // TODO: Call the function/method being tested\n`;
|
||||
template += `\n`;
|
||||
template += ` // Assert\n`;
|
||||
template += ` // TODO: Verify the expected behavior\n`;
|
||||
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
template += ` // TODO: Add more test cases for different scenarios\n`;
|
||||
}
|
||||
|
||||
template += `});\n`;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
function extractEntities(content) {
|
||||
const entities = [];
|
||||
|
||||
// Extract exported functions
|
||||
const functionMatches = content.matchAll(/export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/g);
|
||||
for (const match of functionMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract exported classes
|
||||
const classMatches = content.matchAll(/export\s+class\s+([a-zA-Z0-9_]+)/g);
|
||||
for (const match of classMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract exported const functions (arrow functions)
|
||||
const constMatches = content.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:async\s*)?\(/g);
|
||||
for (const match of constMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
generateTest();
|
||||
}
|
||||
|
||||
module.exports = { generateTest };
|
||||
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Section Removal Utility
|
||||
*
|
||||
* Cleanly removes only the TDD automation section from CLAUDE.md
|
||||
* while preserving all other content.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import utilities
|
||||
const projectRoot = process.cwd();
|
||||
const skillRoot = path.join(__dirname, '..');
|
||||
|
||||
// Try to load ClaudeMdValidator
|
||||
let ClaudeMdValidator;
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
console.error('❌ Error: ClaudeMdValidator not found');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTddSection() {
|
||||
console.log('🗑️ TDD Section Removal Utility\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const validator = new ClaudeMdValidator(projectRoot);
|
||||
|
||||
// Check if CLAUDE.md exists
|
||||
if (!fs.existsSync(validator.claudeMdPath)) {
|
||||
console.log('❌ CLAUDE.md not found');
|
||||
console.log(` Expected location: ${validator.claudeMdPath}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if TDD section exists
|
||||
const content = fs.readFileSync(validator.claudeMdPath, 'utf-8');
|
||||
|
||||
if (!validator.detectTddSection(content)) {
|
||||
console.log('✅ No TDD section found in CLAUDE.md');
|
||||
console.log(' Nothing to remove');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('🔍 TDD section detected in CLAUDE.md\n');
|
||||
|
||||
// Show what will be removed
|
||||
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) {
|
||||
const sectionSize = endIdx - startIdx + endMarker.length;
|
||||
console.log(` Section size: ${formatBytes(sectionSize)}`);
|
||||
console.log(` Total file size: ${formatBytes(content.length)}`);
|
||||
console.log(` After removal: ${formatBytes(content.length - sectionSize)}\n`);
|
||||
}
|
||||
|
||||
// Perform removal
|
||||
console.log('🔒 Creating backup before removal...');
|
||||
const result = validator.removeTddSection();
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Backup created: ${path.basename(result.backup)}\n`);
|
||||
console.log('✅ TDD section removed successfully!\n');
|
||||
console.log(' Statistics:');
|
||||
console.log(` • Original size: ${formatBytes(result.originalSize)}`);
|
||||
console.log(` • New size: ${formatBytes(result.newSize)}`);
|
||||
console.log(` • Removed: ${formatBytes(result.removed)}\n`);
|
||||
console.log(' Backup available at:');
|
||||
console.log(` ${result.backup}\n`);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('⚠️ TDD automation configuration removed from CLAUDE.md\n');
|
||||
console.log(' Other TDD automation components remain:');
|
||||
console.log(' • .tdd-automation/ directory');
|
||||
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||
console.log(' To remove all components:');
|
||||
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||
console.log(' To restore this section:');
|
||||
console.log(` node .tdd-automation/scripts/rollback-tdd.js\n`);
|
||||
} else {
|
||||
console.error('❌ Removal failed:', result.reason);
|
||||
console.error('');
|
||||
console.error(' Possible issues:');
|
||||
console.error(' • Section markers not found (manual edit may be needed)');
|
||||
console.error(' • File permissions issue');
|
||||
console.error(' • Disk space issue');
|
||||
console.error('');
|
||||
console.error(' Manual removal:');
|
||||
console.error(' 1. Open .claude/CLAUDE.md in editor');
|
||||
console.error(' 2. Find <!-- TDD_AUTOMATION_START -->');
|
||||
console.error(' 3. Delete everything until <!-- TDD_AUTOMATION_END -->');
|
||||
console.error(' 4. Save file');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
removeTddSection().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { removeTddSection };
|
||||
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Automation Rollback Utility
|
||||
*
|
||||
* Restores previous CLAUDE.md from backup and optionally removes all TDD automation.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import utilities from parent directory
|
||||
const projectRoot = process.cwd();
|
||||
const skillRoot = path.join(__dirname, '..');
|
||||
|
||||
// Try to load ClaudeMdValidator from skill location
|
||||
let ClaudeMdValidator;
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
// If skill utils not available, use local copy in .tdd-automation
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
console.error('❌ Error: ClaudeMdValidator not found');
|
||||
console.error(' This script must be run from project root or .tdd-automation directory');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function rollbackTddAutomation() {
|
||||
console.log('🔄 TDD Automation Rollback Utility\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const validator = new ClaudeMdValidator(projectRoot);
|
||||
|
||||
// List available backups
|
||||
const backups = validator.listBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log('❌ No backup files found');
|
||||
console.log(' Cannot rollback without backup');
|
||||
console.log('');
|
||||
console.log(' Backup files should be in: .claude/CLAUDE.md.backup.*');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📋 Found ${backups.length} backup(s):\n`);
|
||||
|
||||
backups.forEach((backup, i) => {
|
||||
const age = getTimeAgo(backup.created);
|
||||
console.log(`${i + 1}. ${backup.name}`);
|
||||
console.log(` Created: ${backup.created.toISOString()} (${age})`);
|
||||
console.log(` Size: ${formatBytes(backup.size)}\n`);
|
||||
});
|
||||
|
||||
// Use most recent backup
|
||||
const latestBackup = backups[0];
|
||||
console.log(`🔄 Rolling back to most recent backup:\n`);
|
||||
console.log(` ${latestBackup.name}`);
|
||||
console.log(` Created: ${latestBackup.created.toISOString()}\n`);
|
||||
|
||||
// Create backup of current state before rollback
|
||||
console.log('🔒 Creating safety backup of current state...');
|
||||
const currentBackup = validator.createBackup();
|
||||
|
||||
if (currentBackup.success) {
|
||||
console.log(`✅ Current state backed up to:`);
|
||||
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||
} else {
|
||||
console.log(`⚠️ Could not backup current state: ${currentBackup.reason}`);
|
||||
console.log(` Proceeding with rollback anyway...\n`);
|
||||
}
|
||||
|
||||
// Perform rollback
|
||||
console.log('🔄 Performing rollback...');
|
||||
const result = validator.rollback(latestBackup.path);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Rollback successful!\n');
|
||||
console.log(' CLAUDE.md has been restored from:');
|
||||
console.log(` ${path.basename(result.restoredFrom)}`);
|
||||
console.log(` Size: ${formatBytes(result.size)}\n`);
|
||||
|
||||
if (currentBackup.success) {
|
||||
console.log(' Your previous state was saved to:');
|
||||
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('⚠️ TDD automation configuration has been removed from CLAUDE.md\n');
|
||||
console.log(' Other TDD automation components remain:');
|
||||
console.log(' • .tdd-automation/ directory');
|
||||
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||
console.log(' To remove all TDD automation components:');
|
||||
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||
console.log(' To reinstall TDD automation:');
|
||||
console.log(' Run the tdd-automation skill again\n');
|
||||
} else {
|
||||
console.error('❌ Rollback failed:', result.reason);
|
||||
console.error('');
|
||||
console.error(' You may need to manually restore CLAUDE.md from backup');
|
||||
console.error(` Backup location: ${latestBackup.path}`);
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
rollbackTddAutomation().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { rollbackTddAutomation };
|
||||
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Compliance Validator
|
||||
*
|
||||
* Validates that project follows TDD best practices:
|
||||
* - All implementation files have corresponding tests
|
||||
* - Test coverage meets minimum threshold
|
||||
* - Tests are properly structured
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
async function validateTdd() {
|
||||
console.log('🔍 TDD Compliance Validation\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const results = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// Check 1: Verify test files exist for implementation files
|
||||
console.log('📋 Check 1: Test file coverage...');
|
||||
await checkTestFileCoverage(results);
|
||||
|
||||
// Check 2: Verify test framework is installed
|
||||
console.log('\n📋 Check 2: Test framework installation...');
|
||||
await checkTestFramework(results);
|
||||
|
||||
// Check 3: Verify TDD npm scripts exist
|
||||
console.log('\n📋 Check 3: TDD npm scripts...');
|
||||
await checkNpmScripts(results);
|
||||
|
||||
// Check 4: Verify git hooks installed
|
||||
console.log('\n📋 Check 4: Git hooks...');
|
||||
await checkGitHooks(results);
|
||||
|
||||
// Check 5: Verify CLAUDE.md has TDD configuration
|
||||
console.log('\n📋 Check 5: CLAUDE.md configuration...');
|
||||
await checkClaudeMd(results);
|
||||
|
||||
// Summary
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('📊 Validation Summary\n');
|
||||
|
||||
if (results.passed.length > 0) {
|
||||
console.log(`✅ Passed: ${results.passed.length}`);
|
||||
results.passed.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (results.warnings.length > 0) {
|
||||
console.log(`⚠️ Warnings: ${results.warnings.length}`);
|
||||
results.warnings.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log(`❌ Failed: ${results.failed.length}`);
|
||||
results.failed.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log('❌ TDD compliance validation FAILED\n');
|
||||
console.log(' Please address the failed checks above');
|
||||
console.log(' Run: npm run generate:test <file> to create missing tests\n');
|
||||
process.exit(1);
|
||||
} else if (results.warnings.length > 0) {
|
||||
console.log('⚠️ TDD compliance validation PASSED with warnings\n');
|
||||
console.log(' Consider addressing the warnings above\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('✅ TDD compliance validation PASSED\n');
|
||||
console.log(' All checks passed successfully!\n');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTestFileCoverage(results) {
|
||||
const srcDir = path.join(process.cwd(), 'src');
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
results.warnings.push('No src/ directory found - skipping test coverage check');
|
||||
return;
|
||||
}
|
||||
|
||||
const implFiles = findImplementationFiles(srcDir);
|
||||
const testFiles = findTestFiles(srcDir);
|
||||
|
||||
let missing = 0;
|
||||
let covered = 0;
|
||||
|
||||
for (const implFile of implFiles) {
|
||||
const testFile = findCorrespondingTest(implFile, testFiles);
|
||||
|
||||
if (!testFile) {
|
||||
missing++;
|
||||
if (missing <= 5) { // Only show first 5 to avoid spam
|
||||
results.failed.push(`Missing test for: ${path.relative(process.cwd(), implFile)}`);
|
||||
}
|
||||
} else {
|
||||
covered++;
|
||||
}
|
||||
}
|
||||
|
||||
if (missing > 5) {
|
||||
results.failed.push(`... and ${missing - 5} more files without tests`);
|
||||
}
|
||||
|
||||
const totalFiles = implFiles.length;
|
||||
const coverage = totalFiles > 0 ? ((covered / totalFiles) * 100).toFixed(1) : 100;
|
||||
|
||||
if (missing === 0 && totalFiles > 0) {
|
||||
results.passed.push(`All ${totalFiles} implementation files have tests (100%)`);
|
||||
} else if (coverage >= 80) {
|
||||
results.warnings.push(`Test file coverage: ${coverage}% (${covered}/${totalFiles}) - below 100%`);
|
||||
} else if (totalFiles === 0) {
|
||||
results.warnings.push('No implementation files found in src/');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTestFramework(results) {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
results.failed.push('package.json not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
const testFrameworks = ['vitest', 'jest', '@jest/globals', 'mocha', 'ava'];
|
||||
const installed = testFrameworks.find(fw => allDeps[fw]);
|
||||
|
||||
if (installed) {
|
||||
results.passed.push(`Test framework installed: ${installed}`);
|
||||
} else {
|
||||
results.failed.push('No test framework found (install vitest, jest, or mocha)');
|
||||
}
|
||||
} catch (error) {
|
||||
results.failed.push(`Error reading package.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNpmScripts(results) {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return; // Already reported in previous check
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const scripts = packageJson.scripts || {};
|
||||
|
||||
const requiredScripts = ['test:tdd', 'validate:tdd', 'generate:test'];
|
||||
const missingScripts = requiredScripts.filter(script => !scripts[script]);
|
||||
|
||||
if (missingScripts.length === 0) {
|
||||
results.passed.push('All TDD npm scripts installed');
|
||||
} else {
|
||||
missingScripts.forEach(script => {
|
||||
results.warnings.push(`Missing npm script: ${script}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
results.failed.push(`Error checking npm scripts: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitHooks(results) {
|
||||
const preCommitPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
|
||||
|
||||
if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
|
||||
results.warnings.push('Not a git repository - no git hooks checked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(preCommitPath)) {
|
||||
results.warnings.push('Git pre-commit hook not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
||||
|
||||
if (content.includes('TDD_AUTOMATION') || content.includes('TDD Validation')) {
|
||||
results.passed.push('Git pre-commit hook installed for TDD validation');
|
||||
} else {
|
||||
results.warnings.push('Git pre-commit hook exists but may not have TDD validation');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkClaudeMd(results) {
|
||||
const claudeMdPath = path.join(process.cwd(), '.claude', 'CLAUDE.md');
|
||||
|
||||
if (!fs.existsSync(claudeMdPath)) {
|
||||
results.warnings.push('No .claude/CLAUDE.md found');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
||||
|
||||
const tddMarkers = [
|
||||
'TDD Red-Green-Refactor',
|
||||
'tdd-automation-version',
|
||||
'<!-- TDD_AUTOMATION_START -->'
|
||||
];
|
||||
|
||||
const hasTddConfig = tddMarkers.some(marker => content.includes(marker));
|
||||
|
||||
if (hasTddConfig) {
|
||||
results.passed.push('CLAUDE.md has TDD configuration');
|
||||
} else {
|
||||
results.warnings.push('CLAUDE.md exists but missing TDD configuration');
|
||||
}
|
||||
}
|
||||
|
||||
function findImplementationFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip node_modules, dist, build, etc.
|
||||
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||
findImplementationFiles(fullPath, files);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
// Include .ts, .js, .tsx, .jsx files but exclude test files
|
||||
if (/\.(ts|js|tsx|jsx)$/.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function findTestFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||
findTestFiles(fullPath, files);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function findCorrespondingTest(implFile, testFiles) {
|
||||
const dir = path.dirname(implFile);
|
||||
const filename = path.basename(implFile);
|
||||
const base = filename.replace(/\.(ts|js|tsx|jsx)$/, '');
|
||||
|
||||
// Try multiple test file patterns
|
||||
const patterns = [
|
||||
path.join(dir, `${base}.test.ts`),
|
||||
path.join(dir, `${base}.test.js`),
|
||||
path.join(dir, `${base}.test.tsx`),
|
||||
path.join(dir, `${base}.test.jsx`),
|
||||
path.join(dir, `${base}.spec.ts`),
|
||||
path.join(dir, `${base}.spec.js`),
|
||||
path.join(dir, `${base}.spec.tsx`),
|
||||
path.join(dir, `${base}.spec.jsx`)
|
||||
];
|
||||
|
||||
return testFiles.find(testFile => patterns.includes(testFile));
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
validateTdd().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { validateTdd };
|
||||
110
skills/tdd-automation/templates/pre-commit.sh
Executable file
110
skills/tdd-automation/templates/pre-commit.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# TDD_AUTOMATION: Git Pre-Commit Hook
|
||||
#
|
||||
# Validates that tests exist for implementation files before allowing commit.
|
||||
# Ensures TDD workflow (test-first) is followed.
|
||||
#
|
||||
# Installed by: tdd-automation skill
|
||||
###############################################################################
|
||||
|
||||
echo "🔍 TDD Validation: Checking test-first compliance..."
|
||||
|
||||
# Get list of new implementation files being committed
|
||||
IMPL_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E "^src/.*\.(ts|js|tsx|jsx)$" | grep -v ".test." | grep -v ".spec.")
|
||||
|
||||
if [ -z "$IMPL_FILES" ]; then
|
||||
echo "✅ No new implementation files detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VIOLATIONS=0
|
||||
|
||||
# Check each implementation file
|
||||
while IFS= read -r impl_file; do
|
||||
if [ -z "$impl_file" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Determine expected test file name
|
||||
dir=$(dirname "$impl_file")
|
||||
filename=$(basename "$impl_file")
|
||||
base="${filename%.*}"
|
||||
ext="${filename##*.}"
|
||||
|
||||
# Try multiple test file patterns
|
||||
test_patterns=(
|
||||
"$dir/$base.test.$ext"
|
||||
"$dir/$base.spec.$ext"
|
||||
"${impl_file%.*}.test.$ext"
|
||||
"${impl_file%.*}.spec.$ext"
|
||||
)
|
||||
|
||||
test_exists=false
|
||||
for test_file in "${test_patterns[@]}"; do
|
||||
if [ -f "$test_file" ]; then
|
||||
test_exists=true
|
||||
|
||||
# Check if test file was committed first (git log timestamps)
|
||||
test_commit_time=$(git log --diff-filter=A --format="%at" --follow -- "$test_file" 2>/dev/null | head -1)
|
||||
impl_commit_time=$(date +%s) # Current time for uncommitted file
|
||||
|
||||
# If test file exists in git history, it was committed first - good!
|
||||
if [ -n "$test_commit_time" ]; then
|
||||
echo "✅ $impl_file -> Test exists: $test_file"
|
||||
break
|
||||
else
|
||||
# Test file exists but not committed yet
|
||||
if git diff --cached --name-only | grep -q "$test_file"; then
|
||||
echo "✅ $impl_file -> Test being committed: $test_file"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$test_exists" = false ]; then
|
||||
echo "❌ TDD VIOLATION: No test file found for $impl_file"
|
||||
echo " Expected one of:"
|
||||
for pattern in "${test_patterns[@]}"; do
|
||||
echo " - $pattern"
|
||||
done
|
||||
VIOLATIONS=$((VIOLATIONS + 1))
|
||||
fi
|
||||
done <<< "$IMPL_FILES"
|
||||
|
||||
# Check for test files in commit
|
||||
TEST_FILES=$(git diff --cached --name-only | grep -E "\.(test|spec)\.(ts|js|tsx|jsx)$")
|
||||
|
||||
if [ -n "$TEST_FILES" ]; then
|
||||
echo ""
|
||||
echo "📝 Test files in this commit:"
|
||||
echo "$TEST_FILES" | while IFS= read -r test_file; do
|
||||
echo " ✓ $test_file"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ $VIOLATIONS -gt 0 ]; then
|
||||
echo "❌ TDD Validation Failed: $VIOLATIONS violation(s) found"
|
||||
echo ""
|
||||
echo "TDD requires writing tests BEFORE implementation."
|
||||
echo ""
|
||||
echo "To fix:"
|
||||
echo " 1. Create test file(s) for the implementation"
|
||||
echo " 2. Commit tests first: git add <test-files> && git commit -m 'Add tests for [feature]'"
|
||||
echo " 3. Then commit implementation: git add <impl-files> && git commit -m 'Implement [feature]'"
|
||||
echo ""
|
||||
echo "Or use: npm run generate:test <impl-file> to create a test template"
|
||||
echo ""
|
||||
echo "To bypass this check (not recommended):"
|
||||
echo " git commit --no-verify"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ TDD Validation Passed: All implementation files have tests"
|
||||
echo ""
|
||||
exit 0
|
||||
78
skills/tdd-automation/templates/tdd-auto-enforcer.sh
Executable file
78
skills/tdd-automation/templates/tdd-auto-enforcer.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# TDD Auto-Enforcer Hook
|
||||
#
|
||||
# Claude Code hook that automatically guides LLM to follow TDD workflow
|
||||
# when implementing features.
|
||||
#
|
||||
# Installed by: tdd-automation skill
|
||||
###############################################################################
|
||||
|
||||
# Get user's prompt
|
||||
USER_PROMPT="$1"
|
||||
|
||||
# Keywords that indicate implementation work
|
||||
IMPLEMENTATION_KEYWORDS="implement|add|create|build|feature|write.*code|develop|make.*function|make.*component"
|
||||
|
||||
# Check if prompt is about implementing something
|
||||
if echo "$USER_PROMPT" | grep -qiE "$IMPLEMENTATION_KEYWORDS"; then
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
⚠️ TDD ENFORCEMENT ACTIVE
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
You are about to implement code. You MUST follow TDD red-green-refactor:
|
||||
|
||||
📋 Required Workflow:
|
||||
|
||||
1. 🔴 RED Phase (First - DO NOT SKIP)
|
||||
└─ Write a FAILING test before any implementation
|
||||
└─ Run test to verify it fails: npm run test:red <test-file>
|
||||
└─ Use TodoWrite to track this phase
|
||||
|
||||
2. 🟢 GREEN Phase (After RED verified)
|
||||
└─ Write MINIMAL code to make test pass
|
||||
└─ Run test to verify it passes: npm run test:green <test-file>
|
||||
└─ Use TodoWrite to track this phase
|
||||
|
||||
3. 🔵 REFACTOR Phase (After GREEN verified)
|
||||
└─ Improve code quality while keeping tests green
|
||||
└─ Run all tests: npm run test:tdd
|
||||
└─ Use TodoWrite to track this phase
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🚨 CRITICAL RULES
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
❌ NEVER write implementation code before writing the test
|
||||
❌ NEVER skip RED phase verification (must see test fail)
|
||||
❌ NEVER skip GREEN phase verification (must see test pass)
|
||||
✅ ALWAYS use TodoWrite to track RED-GREEN-REFACTOR phases
|
||||
✅ ALWAYS run tests to verify each phase transition
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📝 TodoWrite Template (Use This)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Create a todo list with these phases before starting:
|
||||
|
||||
[ ] RED: Write failing test for [feature name]
|
||||
[ ] Verify test fails with expected error
|
||||
[ ] GREEN: Implement minimal code to pass test
|
||||
[ ] Verify test passes
|
||||
[ ] REFACTOR: Improve code quality
|
||||
[ ] Verify all tests still pass
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Start with RED phase. Create the test file first.
|
||||
|
||||
EOF
|
||||
|
||||
fi
|
||||
|
||||
# Exit successfully (allow prompt to continue)
|
||||
exit 0
|
||||
163
skills/tdd-automation/utils/detect-project-type.js
Normal file
163
skills/tdd-automation/utils/detect-project-type.js
Normal 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;
|
||||
305
skills/tdd-automation/utils/install-hooks.js
Normal file
305
skills/tdd-automation/utils/install-hooks.js
Normal 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;
|
||||
286
skills/tdd-automation/utils/merge-claude-md.js
Normal file
286
skills/tdd-automation/utils/merge-claude-md.js
Normal 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;
|
||||
186
skills/tdd-automation/utils/update-package-json.js
Normal file
186
skills/tdd-automation/utils/update-package-json.js
Normal 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;
|
||||
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