@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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "wr-tdd",
3
+ "version": "0.1.0",
4
+ "description": "TDD state machine enforcement (IDLE/RED/GREEN/BLOCKED) for Claude Code"
5
+ }
@@ -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
+ }
@@ -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