@windyroad/tdd 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +5 -0
- package/bin/install.mjs +42 -0
- package/hooks/hooks.json +16 -0
- package/hooks/lib/tdd-gate.sh +176 -0
- package/hooks/tdd-enforce-edit.sh +58 -0
- package/hooks/tdd-inject.sh +73 -0
- package/hooks/tdd-post-write.sh +114 -0
- package/hooks/tdd-reset.sh +16 -0
- package/lib/install-utils.mjs +163 -0
- package/package.json +29 -0
- package/skills/wr:tdd/SKILL.md +109 -0
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
|
|
8
|
+
|
|
9
|
+
const PLUGIN = "wr-tdd";
|
|
10
|
+
const DEPS = [];
|
|
11
|
+
|
|
12
|
+
const flags = utils.parseStandardArgs(process.argv);
|
|
13
|
+
|
|
14
|
+
if (flags.help) {
|
|
15
|
+
console.log(`
|
|
16
|
+
Usage: npx @windyroad/tdd [options]
|
|
17
|
+
|
|
18
|
+
TDD state machine enforcement (Red-Green-Refactor cycle)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--update Update this plugin and its skills
|
|
22
|
+
--uninstall Remove this plugin
|
|
23
|
+
--dry-run Show what would be done without executing
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (flags.dryRun) {
|
|
30
|
+
utils.setDryRun(true);
|
|
31
|
+
console.log("[dry-run mode — no commands will be executed]\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
utils.checkPrerequisites();
|
|
35
|
+
|
|
36
|
+
if (flags.uninstall) {
|
|
37
|
+
utils.uninstallPackage(PLUGIN);
|
|
38
|
+
} else if (flags.update) {
|
|
39
|
+
utils.updatePackage(PLUGIN);
|
|
40
|
+
} else {
|
|
41
|
+
utils.installPackage(PLUGIN, { deps: DEPS });
|
|
42
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-inject.sh" }] }
|
|
5
|
+
],
|
|
6
|
+
"PreToolUse": [
|
|
7
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-enforce-edit.sh" }] }
|
|
8
|
+
],
|
|
9
|
+
"PostToolUse": [
|
|
10
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] }
|
|
11
|
+
],
|
|
12
|
+
"Stop": [
|
|
13
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-reset.sh" }] }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TDD Gate - shared library for TDD enforcement hooks.
|
|
3
|
+
# Sourced by tdd-inject.sh, tdd-enforce-edit.sh, tdd-post-write.sh, tdd-reset.sh.
|
|
4
|
+
# Provides: tdd_classify_file, tdd_read_state, tdd_write_state, tdd_run_tests,
|
|
5
|
+
# tdd_add_test_file, tdd_get_test_files, tdd_cleanup, tdd_has_test_script,
|
|
6
|
+
# tdd_deny_json
|
|
7
|
+
|
|
8
|
+
# --- Configuration ---
|
|
9
|
+
TDD_TEST_CMD="${TDD_TEST_CMD:-npm test --}"
|
|
10
|
+
TDD_TEST_TIMEOUT="${TDD_TEST_TIMEOUT:-30}"
|
|
11
|
+
|
|
12
|
+
# --- File Classification ---
|
|
13
|
+
# Returns: "test", "exempt", or "impl"
|
|
14
|
+
tdd_classify_file() {
|
|
15
|
+
local FILE_PATH="$1"
|
|
16
|
+
local BASENAME
|
|
17
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
18
|
+
|
|
19
|
+
# Test files (always allowed)
|
|
20
|
+
case "$BASENAME" in
|
|
21
|
+
*.test.ts|*.test.tsx|*.test.js|*.test.jsx) echo "test"; return ;;
|
|
22
|
+
*.spec.ts|*.spec.tsx|*.spec.js|*.spec.jsx) echo "test"; return ;;
|
|
23
|
+
esac
|
|
24
|
+
case "$FILE_PATH" in
|
|
25
|
+
*/__tests__/*) echo "test"; return ;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
# Exempt files (not gated)
|
|
29
|
+
case "$FILE_PATH" in
|
|
30
|
+
# Config files
|
|
31
|
+
*.config.*|*.json|*.yml|*.yaml) echo "exempt"; return ;;
|
|
32
|
+
# Module configs (*.mjs, *.cjs are config when at root or named as config)
|
|
33
|
+
*.mjs|*.cjs) echo "exempt"; return ;;
|
|
34
|
+
# Styles
|
|
35
|
+
*.css|*.scss|*.sass|*.less) echo "exempt"; return ;;
|
|
36
|
+
# Assets
|
|
37
|
+
*.png|*.jpg|*.jpeg|*.gif|*.svg|*.ico|*.webp) echo "exempt"; return ;;
|
|
38
|
+
*.woff|*.woff2|*.ttf|*.eot) echo "exempt"; return ;;
|
|
39
|
+
# Docs
|
|
40
|
+
*.md|*.mdx) echo "exempt"; return ;;
|
|
41
|
+
*/docs/*|docs/*) echo "exempt"; return ;;
|
|
42
|
+
# Tooling
|
|
43
|
+
*/.claude/*|.claude/*) echo "exempt"; return ;;
|
|
44
|
+
*/.github/*|.github/*) echo "exempt"; return ;;
|
|
45
|
+
# Lockfiles and sourcemaps
|
|
46
|
+
*package-lock.json|*yarn.lock|*pnpm-lock.yaml) echo "exempt"; return ;;
|
|
47
|
+
*.map) echo "exempt"; return ;;
|
|
48
|
+
# Shell scripts
|
|
49
|
+
*.sh) echo "exempt"; return ;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
# Implementation files (gated)
|
|
53
|
+
case "$BASENAME" in
|
|
54
|
+
*.ts|*.tsx|*.js|*.jsx) echo "impl"; return ;;
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
# Everything else is exempt
|
|
58
|
+
echo "exempt"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# --- State Management ---
|
|
62
|
+
# Marker files use session ID to isolate concurrent sessions
|
|
63
|
+
_tdd_state_file() { echo "/tmp/tdd-state-${1}"; }
|
|
64
|
+
_tdd_test_files_file() { echo "/tmp/tdd-test-files-${1}"; }
|
|
65
|
+
_tdd_test_stdout_file() { echo "/tmp/tdd-test-stdout-${1}"; }
|
|
66
|
+
|
|
67
|
+
# Read current state. Returns: IDLE, RED, GREEN, or BLOCKED
|
|
68
|
+
tdd_read_state() {
|
|
69
|
+
local SESSION_ID="$1"
|
|
70
|
+
local STATE_FILE
|
|
71
|
+
STATE_FILE=$(_tdd_state_file "$SESSION_ID")
|
|
72
|
+
if [ -f "$STATE_FILE" ]; then
|
|
73
|
+
cat "$STATE_FILE"
|
|
74
|
+
else
|
|
75
|
+
echo "IDLE"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Write state
|
|
80
|
+
tdd_write_state() {
|
|
81
|
+
local SESSION_ID="$1"
|
|
82
|
+
local NEW_STATE="$2"
|
|
83
|
+
local STATE_FILE
|
|
84
|
+
STATE_FILE=$(_tdd_state_file "$SESSION_ID")
|
|
85
|
+
echo "$NEW_STATE" > "$STATE_FILE"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Track test files touched this session
|
|
89
|
+
tdd_add_test_file() {
|
|
90
|
+
local SESSION_ID="$1"
|
|
91
|
+
local TEST_FILE="$2"
|
|
92
|
+
local TRACK_FILE
|
|
93
|
+
TRACK_FILE=$(_tdd_test_files_file "$SESSION_ID")
|
|
94
|
+
# Avoid duplicates
|
|
95
|
+
if [ -f "$TRACK_FILE" ] && grep -qxF "$TEST_FILE" "$TRACK_FILE" 2>/dev/null; then
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
echo "$TEST_FILE" >> "$TRACK_FILE"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Get all test files for this session (newline-separated)
|
|
102
|
+
tdd_get_test_files() {
|
|
103
|
+
local SESSION_ID="$1"
|
|
104
|
+
local TRACK_FILE
|
|
105
|
+
TRACK_FILE=$(_tdd_test_files_file "$SESSION_ID")
|
|
106
|
+
if [ -f "$TRACK_FILE" ]; then
|
|
107
|
+
cat "$TRACK_FILE"
|
|
108
|
+
fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# --- Test Runner ---
|
|
112
|
+
# Runs tests for the session's tracked test files.
|
|
113
|
+
# Returns: 0=pass, 1=fail, 124=timeout, other=error
|
|
114
|
+
# Saves stdout to marker file for debugging.
|
|
115
|
+
tdd_run_tests() {
|
|
116
|
+
local SESSION_ID="$1"
|
|
117
|
+
local STDOUT_FILE
|
|
118
|
+
STDOUT_FILE=$(_tdd_test_stdout_file "$SESSION_ID")
|
|
119
|
+
local TEST_FILES
|
|
120
|
+
TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
|
|
121
|
+
|
|
122
|
+
if [ -z "$TEST_FILES" ]; then
|
|
123
|
+
echo "No test files tracked" > "$STDOUT_FILE"
|
|
124
|
+
return 0 # No tests to run = pass (no-op)
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Build argument list from tracked test files
|
|
128
|
+
local ARGS=""
|
|
129
|
+
while IFS= read -r f; do
|
|
130
|
+
ARGS="$ARGS $f"
|
|
131
|
+
done <<< "$TEST_FILES"
|
|
132
|
+
|
|
133
|
+
# Run with timeout
|
|
134
|
+
local EXIT_CODE
|
|
135
|
+
timeout "$TDD_TEST_TIMEOUT" bash -c "$TDD_TEST_CMD $ARGS" > "$STDOUT_FILE" 2>&1
|
|
136
|
+
EXIT_CODE=$?
|
|
137
|
+
|
|
138
|
+
return $EXIT_CODE
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# --- Prerequisite Check ---
|
|
142
|
+
# Returns 0 if a test script is configured, 1 otherwise
|
|
143
|
+
tdd_has_test_script() {
|
|
144
|
+
if [ -f "package.json" ]; then
|
|
145
|
+
# Check if "test" script exists and is not the default npm placeholder
|
|
146
|
+
local TEST_SCRIPT
|
|
147
|
+
TEST_SCRIPT=$(jq -r '.scripts.test // empty' package.json 2>/dev/null)
|
|
148
|
+
if [ -n "$TEST_SCRIPT" ] && [ "$TEST_SCRIPT" != "echo \"Error: no test specified\" && exit 1" ]; then
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# --- Cleanup ---
|
|
156
|
+
tdd_cleanup() {
|
|
157
|
+
local SESSION_ID="$1"
|
|
158
|
+
rm -f "$(_tdd_state_file "$SESSION_ID")"
|
|
159
|
+
rm -f "$(_tdd_test_files_file "$SESSION_ID")"
|
|
160
|
+
rm -f "$(_tdd_test_stdout_file "$SESSION_ID")"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# --- Deny Helper ---
|
|
164
|
+
# Emit PreToolUse deny JSON
|
|
165
|
+
tdd_deny_json() {
|
|
166
|
+
local REASON="$1"
|
|
167
|
+
cat <<EOF
|
|
168
|
+
{
|
|
169
|
+
"hookSpecificOutput": {
|
|
170
|
+
"hookEventName": "PreToolUse",
|
|
171
|
+
"permissionDecision": "deny",
|
|
172
|
+
"permissionDecisionReason": "$REASON"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
EOF
|
|
176
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TDD - PreToolUse enforcement hook (Edit|Write)
|
|
3
|
+
# Blocks implementation file edits unless state is RED or GREEN.
|
|
4
|
+
# Test files and exempt files are always allowed.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/tdd-gate.sh"
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') || true
|
|
12
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
13
|
+
|
|
14
|
+
if [ -z "$FILE_PATH" ]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Classify first, then check test script (only impl files need gating)
|
|
19
|
+
FILE_TYPE=$(tdd_classify_file "$FILE_PATH")
|
|
20
|
+
if [ "$FILE_TYPE" != "impl" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# If no test script configured, block and direct to create skill
|
|
25
|
+
if ! tdd_has_test_script; then
|
|
26
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
27
|
+
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' because no test script is configured in package.json. Run /wr-tdd:create to set up a test framework for this project. TDD enforcement requires a working test runner before implementation code can be written."
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [ -z "$SESSION_ID" ]; then
|
|
32
|
+
# Fail-closed: cannot check state without session ID
|
|
33
|
+
tdd_deny_json "BLOCKED: Could not determine session ID. TDD gate is fail-closed."
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Check state for implementation files
|
|
38
|
+
STATE=$(tdd_read_state "$SESSION_ID")
|
|
39
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
40
|
+
|
|
41
|
+
case "$STATE" in
|
|
42
|
+
RED|GREEN)
|
|
43
|
+
# Allowed: agent is in the TDD cycle
|
|
44
|
+
exit 0
|
|
45
|
+
;;
|
|
46
|
+
IDLE)
|
|
47
|
+
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- no tests written yet. TDD state is IDLE. You MUST write a failing test first (*.test.ts or *.spec.ts) before editing implementation files. The test should describe the behavior you want to implement."
|
|
48
|
+
exit 0
|
|
49
|
+
;;
|
|
50
|
+
BLOCKED)
|
|
51
|
+
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- test runner is in error state. TDD state is BLOCKED. Fix the test setup first (check test configuration, fix syntax errors in test files, or verify the test command works)."
|
|
52
|
+
exit 0
|
|
53
|
+
;;
|
|
54
|
+
*)
|
|
55
|
+
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- unknown TDD state '${STATE}'. Write a failing test first."
|
|
56
|
+
exit 0
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TDD - UserPromptSubmit hook
|
|
3
|
+
# Injects TDD instructions and current state into every prompt.
|
|
4
|
+
# Only active when a test script is configured in package.json.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/tdd-gate.sh"
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
11
|
+
|
|
12
|
+
# If no test script configured, inject setup instructions
|
|
13
|
+
if ! tdd_has_test_script; then
|
|
14
|
+
cat <<'HOOK_OUTPUT'
|
|
15
|
+
INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
|
|
16
|
+
|
|
17
|
+
This project has NO test script configured in package.json. Implementation file
|
|
18
|
+
edits (.ts, .tsx, .js, .jsx) are BLOCKED until testing is set up.
|
|
19
|
+
|
|
20
|
+
If the user's task involves writing or editing implementation code, you MUST
|
|
21
|
+
run /wr-tdd:create first to configure a test framework for this project.
|
|
22
|
+
|
|
23
|
+
Test files, config files, docs, and styles are still writable.
|
|
24
|
+
HOOK_OUTPUT
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
STATE="IDLE"
|
|
29
|
+
if [ -n "$SESSION_ID" ]; then
|
|
30
|
+
STATE=$(tdd_read_state "$SESSION_ID")
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
TEST_FILES=""
|
|
34
|
+
if [ -n "$SESSION_ID" ]; then
|
|
35
|
+
TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
cat <<HOOK_OUTPUT
|
|
39
|
+
INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
|
|
40
|
+
|
|
41
|
+
This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${STATE}**
|
|
42
|
+
|
|
43
|
+
STATE RULES:
|
|
44
|
+
- IDLE: You MUST write a failing test FIRST before any implementation code.
|
|
45
|
+
Implementation file edits (.ts, .tsx, .js, .jsx) are BLOCKED until you write a test.
|
|
46
|
+
- RED: Tests are failing. Write implementation code to make them pass.
|
|
47
|
+
Implementation file edits are ALLOWED.
|
|
48
|
+
- GREEN: Tests are passing. You may refactor or write a new failing test.
|
|
49
|
+
Implementation file edits are ALLOWED.
|
|
50
|
+
- BLOCKED: Test runner error or timeout. Fix the test setup before continuing.
|
|
51
|
+
Implementation file edits are BLOCKED.
|
|
52
|
+
|
|
53
|
+
WORKFLOW:
|
|
54
|
+
1. Write a test file (*.test.ts, *.spec.ts, etc.) that describes the desired behavior
|
|
55
|
+
2. The test MUST fail (RED state) -- this proves the test is meaningful
|
|
56
|
+
3. Write the minimum implementation to make the test pass (GREEN state)
|
|
57
|
+
4. Refactor while keeping tests green
|
|
58
|
+
5. Repeat for the next behavior
|
|
59
|
+
|
|
60
|
+
IMPORTANT:
|
|
61
|
+
- Test files and config/doc/style files are ALWAYS writable regardless of state
|
|
62
|
+
- Implementation files are ONLY writable in RED or GREEN states
|
|
63
|
+
- The hook runs your tests automatically after every file write
|
|
64
|
+
- To refactor existing code, touch the relevant test file first to enter the cycle
|
|
65
|
+
HOOK_OUTPUT
|
|
66
|
+
|
|
67
|
+
if [ -n "$TEST_FILES" ]; then
|
|
68
|
+
echo ""
|
|
69
|
+
echo "TRACKED TEST FILES THIS SESSION:"
|
|
70
|
+
echo "$TEST_FILES" | sed 's/^/ - /'
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
exit 0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TDD - PostToolUse hook (Edit|Write)
|
|
3
|
+
# Runs tests after file writes and transitions state.
|
|
4
|
+
# Emits additionalContext with the current TDD state.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/tdd-gate.sh"
|
|
8
|
+
|
|
9
|
+
# Skip if no test script configured
|
|
10
|
+
if ! tdd_has_test_script; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') || true
|
|
17
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
18
|
+
|
|
19
|
+
if [ -z "$FILE_PATH" ] || [ -z "$SESSION_ID" ]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Classify the file
|
|
24
|
+
FILE_TYPE=$(tdd_classify_file "$FILE_PATH")
|
|
25
|
+
|
|
26
|
+
# Skip exempt files entirely
|
|
27
|
+
if [ "$FILE_TYPE" = "exempt" ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Track test files
|
|
32
|
+
if [ "$FILE_TYPE" = "test" ]; then
|
|
33
|
+
tdd_add_test_file "$SESSION_ID" "$FILE_PATH"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Run tests
|
|
37
|
+
tdd_run_tests "$SESSION_ID"
|
|
38
|
+
TEST_EXIT=$?
|
|
39
|
+
|
|
40
|
+
# Read current state
|
|
41
|
+
OLD_STATE=$(tdd_read_state "$SESSION_ID")
|
|
42
|
+
|
|
43
|
+
# Transition state based on file type + test result
|
|
44
|
+
NEW_STATE="$OLD_STATE"
|
|
45
|
+
|
|
46
|
+
if [ $TEST_EXIT -eq 0 ]; then
|
|
47
|
+
# Tests pass
|
|
48
|
+
NEW_STATE="GREEN"
|
|
49
|
+
elif [ $TEST_EXIT -eq 1 ]; then
|
|
50
|
+
# Tests fail
|
|
51
|
+
case "$FILE_TYPE" in
|
|
52
|
+
test)
|
|
53
|
+
NEW_STATE="RED"
|
|
54
|
+
;;
|
|
55
|
+
impl)
|
|
56
|
+
case "$OLD_STATE" in
|
|
57
|
+
RED) NEW_STATE="RED" ;; # still working
|
|
58
|
+
GREEN) NEW_STATE="RED" ;; # broke something
|
|
59
|
+
*) NEW_STATE="RED" ;;
|
|
60
|
+
esac
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
else
|
|
64
|
+
# Timeout (124) or other error
|
|
65
|
+
NEW_STATE="BLOCKED"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Write new state
|
|
69
|
+
tdd_write_state "$SESSION_ID" "$NEW_STATE"
|
|
70
|
+
|
|
71
|
+
# Read last test output for context
|
|
72
|
+
STDOUT_FILE="/tmp/tdd-test-stdout-${SESSION_ID}"
|
|
73
|
+
TEST_OUTPUT=""
|
|
74
|
+
if [ -f "$STDOUT_FILE" ]; then
|
|
75
|
+
# Limit to last 50 lines to avoid flooding context
|
|
76
|
+
TEST_OUTPUT=$(tail -50 "$STDOUT_FILE")
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Emit state as additionalContext
|
|
80
|
+
if [ "$OLD_STATE" != "$NEW_STATE" ]; then
|
|
81
|
+
TRANSITION="State transition: ${OLD_STATE} -> ${NEW_STATE}"
|
|
82
|
+
else
|
|
83
|
+
TRANSITION="State unchanged: ${NEW_STATE}"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
cat <<EOF
|
|
87
|
+
TDD STATE UPDATE: ${TRANSITION}
|
|
88
|
+
Current state: ${NEW_STATE}
|
|
89
|
+
File written: ${FILE_PATH} (${FILE_TYPE})
|
|
90
|
+
Test result: exit code ${TEST_EXIT}
|
|
91
|
+
EOF
|
|
92
|
+
|
|
93
|
+
if [ $TEST_EXIT -ne 0 ] && [ -n "$TEST_OUTPUT" ]; then
|
|
94
|
+
echo ""
|
|
95
|
+
echo "Test output (last 50 lines):"
|
|
96
|
+
echo "$TEST_OUTPUT"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
case "$NEW_STATE" in
|
|
100
|
+
RED)
|
|
101
|
+
echo ""
|
|
102
|
+
echo "ACTION: Tests are failing. Write implementation code to make them pass."
|
|
103
|
+
;;
|
|
104
|
+
GREEN)
|
|
105
|
+
echo ""
|
|
106
|
+
echo "ACTION: Tests are passing. You may refactor or write a new failing test for the next behavior."
|
|
107
|
+
;;
|
|
108
|
+
BLOCKED)
|
|
109
|
+
echo ""
|
|
110
|
+
echo "ACTION: Test runner error (exit code ${TEST_EXIT}). Fix the test setup before continuing."
|
|
111
|
+
;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
114
|
+
exit 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TDD - Stop hook
|
|
3
|
+
# Cleans up TDD marker files when the session ends.
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "$SCRIPT_DIR/lib/tdd-gate.sh"
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
11
|
+
|
|
12
|
+
if [ -n "$SESSION_ID" ]; then
|
|
13
|
+
tdd_cleanup "$SESSION_ID"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exit 0
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared install utilities for @windyroad/* packages.
|
|
3
|
+
* Used by both per-plugin installers and the meta-installer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
|
+
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
|
|
11
|
+
let _dryRun = false;
|
|
12
|
+
|
|
13
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
14
|
+
|
|
15
|
+
export function setDryRun(value) {
|
|
16
|
+
_dryRun = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDryRun() {
|
|
20
|
+
return _dryRun;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function run(cmd, label) {
|
|
24
|
+
console.log(` ${label}...`);
|
|
25
|
+
if (_dryRun) {
|
|
26
|
+
console.log(` [dry-run] ${cmd}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync(cmd, { stdio: "inherit" });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(` FAILED: ${label}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkPrerequisites() {
|
|
39
|
+
if (_dryRun) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
43
|
+
} catch {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
execSync("npx --version", { stdio: "pipe" });
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(
|
|
54
|
+
"Error: 'npx' not found. Install Node.js first:\n https://nodejs.org\n"
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function addMarketplace() {
|
|
61
|
+
return run(
|
|
62
|
+
`claude plugin marketplace add ${MARKETPLACE_REPO}`,
|
|
63
|
+
`Marketplace: ${MARKETPLACE_NAME}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function installPlugin(pluginName) {
|
|
68
|
+
return run(
|
|
69
|
+
`claude plugin install ${pluginName}@${MARKETPLACE_NAME}`,
|
|
70
|
+
pluginName
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function updatePlugin(pluginName) {
|
|
75
|
+
return run(`claude plugin update ${pluginName}`, pluginName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function uninstallPlugin(pluginName) {
|
|
79
|
+
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function installSkills() {
|
|
83
|
+
return run(
|
|
84
|
+
`npx -y skills add --yes --all ${MARKETPLACE_REPO}`,
|
|
85
|
+
"Skills (via skills package)"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function updateSkills() {
|
|
90
|
+
return run("npx -y skills update", "Skills update");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeSkills() {
|
|
94
|
+
return run(
|
|
95
|
+
`npx -y skills remove --yes --all ${MARKETPLACE_REPO}`,
|
|
96
|
+
"Removing skills"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Install a single package: marketplace add + plugin install + skills.
|
|
102
|
+
*/
|
|
103
|
+
export function installPackage(pluginName, { deps = [] } = {}) {
|
|
104
|
+
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
105
|
+
|
|
106
|
+
addMarketplace();
|
|
107
|
+
installPlugin(pluginName);
|
|
108
|
+
installSkills();
|
|
109
|
+
|
|
110
|
+
if (deps.length > 0) {
|
|
111
|
+
console.log(`\nNote: This plugin works best with:`);
|
|
112
|
+
for (const dep of deps) {
|
|
113
|
+
console.log(` - @windyroad/${dep.replace("wr-", "")} (npx @windyroad/${dep.replace("wr-", "")})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
`\nDone! Restart Claude Code to activate.\n`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update a single package.
|
|
124
|
+
*/
|
|
125
|
+
export function updatePackage(pluginName) {
|
|
126
|
+
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
127
|
+
|
|
128
|
+
run(
|
|
129
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
130
|
+
"Updating marketplace"
|
|
131
|
+
);
|
|
132
|
+
updatePlugin(pluginName);
|
|
133
|
+
updateSkills();
|
|
134
|
+
|
|
135
|
+
console.log("\nDone! Restart Claude Code to apply updates.\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Uninstall a single package.
|
|
140
|
+
*/
|
|
141
|
+
export function uninstallPackage(pluginName) {
|
|
142
|
+
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
143
|
+
|
|
144
|
+
uninstallPlugin(pluginName);
|
|
145
|
+
|
|
146
|
+
console.log("\nDone. Restart Claude Code to apply changes.\n");
|
|
147
|
+
console.log("Note: Skills are shared across packages. Run");
|
|
148
|
+
console.log(" npx @windyroad/agent-plugins --uninstall");
|
|
149
|
+
console.log("to remove all skills.\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse standard flags used by all per-plugin installers.
|
|
154
|
+
*/
|
|
155
|
+
export function parseStandardArgs(argv) {
|
|
156
|
+
const args = argv.slice(2);
|
|
157
|
+
return {
|
|
158
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
159
|
+
uninstall: args.includes("--uninstall"),
|
|
160
|
+
update: args.includes("--update"),
|
|
161
|
+
dryRun: args.includes("--dry-run"),
|
|
162
|
+
};
|
|
163
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@windyroad/tdd",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"windyroad-tdd": "./bin/install.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/windyroad/agent-plugins.git",
|
|
13
|
+
"directory": "packages/tdd"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"claude-code-plugin",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"ai-coding"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"agents/",
|
|
24
|
+
"hooks/",
|
|
25
|
+
"skills/",
|
|
26
|
+
".claude-plugin/",
|
|
27
|
+
"lib/"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wr:tdd
|
|
3
|
+
description: Set up a test framework for the project. Examines the codebase, recommends a test runner, configures package.json, and creates an example test.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Test Framework Setup
|
|
8
|
+
|
|
9
|
+
Configure a test framework for this project so TDD enforcement can operate. The TDD hooks require a working `test` script in package.json to run the Red-Green-Refactor cycle.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
### 1. Discover project context
|
|
14
|
+
|
|
15
|
+
Examine the project to understand what needs testing.
|
|
16
|
+
|
|
17
|
+
**Find the tech stack**:
|
|
18
|
+
- Read `package.json` for dependencies, devDependencies, and existing scripts
|
|
19
|
+
- Check for TypeScript (`tsconfig.json`, `.ts` files)
|
|
20
|
+
- Check for React/Next.js/Vue/Svelte (framework-specific testing needs)
|
|
21
|
+
- Check for existing test infrastructure (vitest.config.*, jest.config.*, etc.)
|
|
22
|
+
- Check for existing test files (`*.test.*`, `*.spec.*`, `__tests__/`)
|
|
23
|
+
|
|
24
|
+
**Assess what exists**:
|
|
25
|
+
- If test files already exist but no test script: just wire up the script
|
|
26
|
+
- If a test runner is installed but not configured: configure it
|
|
27
|
+
- If nothing exists: recommend a test runner and set up from scratch
|
|
28
|
+
|
|
29
|
+
### 2. Choose a test runner
|
|
30
|
+
|
|
31
|
+
Based on the project's stack, recommend the best test runner:
|
|
32
|
+
|
|
33
|
+
| Stack | Recommended Runner | Why |
|
|
34
|
+
|-------|-------------------|-----|
|
|
35
|
+
| Vite/Vitest already installed | Vitest | Already in deps |
|
|
36
|
+
| Next.js / React | Vitest | Fast, ESM-native, good React support |
|
|
37
|
+
| TypeScript project | Vitest | No compile step needed |
|
|
38
|
+
| Plain Node.js | Node.js built-in test runner | Zero dependencies |
|
|
39
|
+
| Existing Jest setup | Jest | Don't switch if already configured |
|
|
40
|
+
|
|
41
|
+
Prefer Vitest for most modern projects. It's fast, needs minimal config, and works with TypeScript and JSX out of the box.
|
|
42
|
+
|
|
43
|
+
### 3. Confirm with the user
|
|
44
|
+
|
|
45
|
+
You MUST use the AskUserQuestion tool before making changes.
|
|
46
|
+
|
|
47
|
+
Present:
|
|
48
|
+
1. What you found (existing test infrastructure, if any)
|
|
49
|
+
2. The recommended test runner and why
|
|
50
|
+
3. What files will be created/modified
|
|
51
|
+
4. Whether they want an example test created
|
|
52
|
+
|
|
53
|
+
### 4. Install and configure
|
|
54
|
+
|
|
55
|
+
Based on user confirmation:
|
|
56
|
+
|
|
57
|
+
**If Vitest:**
|
|
58
|
+
```bash
|
|
59
|
+
npm install -D vitest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Add to package.json scripts:
|
|
63
|
+
```json
|
|
64
|
+
"test": "vitest run"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Create `vitest.config.ts` if needed (minimal config, only what's required).
|
|
68
|
+
|
|
69
|
+
**If Node.js built-in:**
|
|
70
|
+
Add to package.json scripts:
|
|
71
|
+
```json
|
|
72
|
+
"test": "node --test"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**If Jest:**
|
|
76
|
+
```bash
|
|
77
|
+
npm install -D jest @types/jest ts-jest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Configure as needed for the project's TypeScript/JSX setup.
|
|
81
|
+
|
|
82
|
+
### 5. Create example test (optional)
|
|
83
|
+
|
|
84
|
+
If the user wants an example test, create one that:
|
|
85
|
+
- Tests an existing function or component in the project
|
|
86
|
+
- Follows the project's file structure conventions
|
|
87
|
+
- Demonstrates the testing pattern (describe/it/expect)
|
|
88
|
+
- Is intentionally minimal (the user will write real tests)
|
|
89
|
+
|
|
90
|
+
Place it next to the source file it tests, using the `.test.ts` or `.test.tsx` convention.
|
|
91
|
+
|
|
92
|
+
### 6. Verify
|
|
93
|
+
|
|
94
|
+
Run the test command to confirm it works:
|
|
95
|
+
```bash
|
|
96
|
+
npm test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If it fails, diagnose and fix the configuration. The test script must work before TDD enforcement can operate.
|
|
100
|
+
|
|
101
|
+
### 7. Report
|
|
102
|
+
|
|
103
|
+
Tell the user:
|
|
104
|
+
- What was installed and configured
|
|
105
|
+
- How to run tests (`npm test`)
|
|
106
|
+
- That TDD enforcement is now active (implementation edits require tests first)
|
|
107
|
+
- The Red-Green-Refactor workflow they'll follow
|
|
108
|
+
|
|
109
|
+
$ARGUMENTS
|