Initial commit
This commit is contained in:
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