@windyroad/tdd 0.2.0 → 0.2.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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @windyroad/tdd
2
+
3
+ **TDD state machine enforcement for Claude Code.** Forces the Red-Green-Refactor cycle so your AI agent writes tests before implementation -- every time.
4
+
5
+ Part of [Windy Road Agent Plugins](../../README.md).
6
+
7
+ ## What It Does
8
+
9
+ AI agents love to jump straight to implementation. This plugin stops that. It enforces a strict TDD state machine:
10
+
11
+ ```
12
+ IDLE ──> RED ──> GREEN ──> RED (next test)
13
+
14
+ └──> Refactor (staying GREEN)
15
+ ```
16
+
17
+ - **IDLE** -- No test written yet. Implementation file edits are blocked.
18
+ - **RED** -- A failing test exists. Implementation edits are allowed.
19
+ - **GREEN** -- Tests pass. You can refactor or write a new failing test.
20
+ - **BLOCKED** -- Test runner error or timeout. Fix the setup before continuing.
21
+
22
+ The agent must write a failing test first. There are no shortcuts.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npx @windyroad/tdd
28
+ ```
29
+
30
+ Restart Claude Code after installing.
31
+
32
+ ## Usage
33
+
34
+ The plugin activates automatically. On first use in a project without a test framework, it directs you to set one up:
35
+
36
+ ```
37
+ /wr-tdd:setup-tests
38
+ ```
39
+
40
+ This examines your codebase, recommends a test runner, configures `package.json`, and creates an example test.
41
+
42
+ Once active, the workflow is enforced on every edit:
43
+
44
+ 1. Write a test file (`*.test.ts`, `*.spec.ts`, etc.) that describes the desired behaviour
45
+ 2. The test must fail (RED state) -- proving the test is meaningful
46
+ 3. Write the minimum implementation to make it pass (GREEN state)
47
+ 4. Refactor while keeping tests green
48
+ 5. Repeat
49
+
50
+ Test files and config/doc files are always writable regardless of state.
51
+
52
+ ## How It Works
53
+
54
+ | Hook | Trigger | What it does |
55
+ |------|---------|-------------|
56
+ | `tdd-inject.sh` | Every prompt | Injects the current TDD state into the prompt |
57
+ | `tdd-enforce-edit.sh` | Edit or Write | Blocks implementation edits in IDLE or BLOCKED state |
58
+ | `tdd-post-write.sh` | After edit | Runs tests and transitions the state machine |
59
+ | `tdd-setup-marker.sh` | Skill completes | Marks test setup as done |
60
+ | `tdd-reset.sh` | Session end | Resets the TDD state |
61
+
62
+ ## Updating and Uninstalling
63
+
64
+ ```bash
65
+ npx @windyroad/tdd --update
66
+ npx @windyroad/tdd --uninstall
67
+ ```
68
+
69
+ ## Licence
70
+
71
+ [MIT](../../LICENSE)
package/bin/install.mjs CHANGED
@@ -4,7 +4,7 @@ import { resolve, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const utils = await import(resolve(__dirname, "../../shared/install-utils.mjs"));
7
+ const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
8
8
 
9
9
  const PLUGIN = "wr-tdd";
10
10
  const DEPS = [];
@@ -20,6 +20,7 @@ TDD state machine enforcement (Red-Green-Refactor cycle)
20
20
  Options:
21
21
  --update Update this plugin and its skills
22
22
  --uninstall Remove this plugin
23
+ --scope Installation scope: project (default) or user
23
24
  --dry-run Show what would be done without executing
24
25
  --help, -h Show this help
25
26
  `);
@@ -38,5 +39,5 @@ if (flags.uninstall) {
38
39
  } else if (flags.update) {
39
40
  utils.updatePackage(PLUGIN);
40
41
  } else {
41
- utils.installPackage(PLUGIN, { deps: DEPS });
42
+ utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope });
42
43
  }
package/hooks/hooks.json CHANGED
@@ -7,7 +7,8 @@
7
7
  { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-enforce-edit.sh" }] }
8
8
  ],
9
9
  "PostToolUse": [
10
- { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] }
10
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] },
11
+ { "matcher": "Skill", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-setup-marker.sh" }] }
11
12
  ],
12
13
  "Stop": [
13
14
  { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-reset.sh" }] }
@@ -1,9 +1,10 @@
1
1
  #!/bin/bash
2
2
  # TDD Gate - shared library for TDD enforcement hooks.
3
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
4
+ # Provides: tdd_classify_file, tdd_read_state, tdd_write_state, tdd_run_test_file,
5
+ # tdd_add_test_file, tdd_get_test_files, tdd_find_test_for_impl,
6
+ # tdd_read_state_for_impl, tdd_get_all_states, tdd_cleanup,
7
+ # tdd_has_test_script, tdd_deny_json
7
8
 
8
9
  # --- Configuration ---
9
10
  TDD_TEST_CMD="${TDD_TEST_CMD:-npm test --}"
@@ -27,8 +28,8 @@ tdd_classify_file() {
27
28
 
28
29
  # Exempt files (not gated)
29
30
  case "$FILE_PATH" in
30
- # Config files
31
- *.config.*|*.json|*.yml|*.yaml) echo "exempt"; return ;;
31
+ # Config and setup files (test infrastructure)
32
+ *.config.*|*.setup.*|*.json|*.yml|*.yaml) echo "exempt"; return ;;
32
33
  # Module configs (*.mjs, *.cjs are config when at root or named as config)
33
34
  *.mjs|*.cjs) echo "exempt"; return ;;
34
35
  # Styles
@@ -58,17 +59,28 @@ tdd_classify_file() {
58
59
  echo "exempt"
59
60
  }
60
61
 
61
- # --- State Management ---
62
- # Marker files use session ID to isolate concurrent sessions
63
- _tdd_state_file() { echo "/tmp/tdd-state-${1}"; }
62
+ # --- State Management (per-test-file) ---
63
+ # State is stored per test file in a session-scoped directory.
64
+ # Each test file gets its own state: IDLE, RED, GREEN, or BLOCKED.
65
+
66
+ _tdd_state_dir() { echo "/tmp/tdd-state-${1}"; }
64
67
  _tdd_test_files_file() { echo "/tmp/tdd-test-files-${1}"; }
65
68
  _tdd_test_stdout_file() { echo "/tmp/tdd-test-stdout-${1}"; }
66
69
 
67
- # Read current state. Returns: IDLE, RED, GREEN, or BLOCKED
70
+ # Encode a file path for use as a filename (replace / with __)
71
+ _tdd_encode_path() {
72
+ echo "$1" | sed 's|/|__|g'
73
+ }
74
+
75
+ # Read state for a specific test file. Returns: IDLE, RED, GREEN, or BLOCKED
68
76
  tdd_read_state() {
69
77
  local SESSION_ID="$1"
70
- local STATE_FILE
71
- STATE_FILE=$(_tdd_state_file "$SESSION_ID")
78
+ local TEST_FILE="$2"
79
+ local STATE_DIR
80
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
81
+ local ENCODED
82
+ ENCODED=$(_tdd_encode_path "$TEST_FILE")
83
+ local STATE_FILE="${STATE_DIR}/${ENCODED}"
72
84
  if [ -f "$STATE_FILE" ]; then
73
85
  cat "$STATE_FILE"
74
86
  else
@@ -76,13 +88,17 @@ tdd_read_state() {
76
88
  fi
77
89
  }
78
90
 
79
- # Write state
91
+ # Write state for a specific test file
80
92
  tdd_write_state() {
81
93
  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"
94
+ local TEST_FILE="$2"
95
+ local NEW_STATE="$3"
96
+ local STATE_DIR
97
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
98
+ mkdir -p "$STATE_DIR"
99
+ local ENCODED
100
+ ENCODED=$(_tdd_encode_path "$TEST_FILE")
101
+ echo "$NEW_STATE" > "${STATE_DIR}/${ENCODED}"
86
102
  }
87
103
 
88
104
  # Track test files touched this session
@@ -108,31 +124,133 @@ tdd_get_test_files() {
108
124
  fi
109
125
  }
110
126
 
127
+ # --- Impl-to-Test Association ---
128
+ # Given an implementation file, find its associated test file from tracked tests.
129
+ # Convention: Hero.tsx ↔ Hero.test.tsx or Hero.spec.tsx (same dir or __tests__/)
130
+ # Returns the first matching tracked test file, or empty string if none.
131
+ tdd_find_test_for_impl() {
132
+ local SESSION_ID="$1"
133
+ local IMPL_PATH="$2"
134
+ local DIR BASENAME STEM EXT
135
+ DIR=$(dirname "$IMPL_PATH")
136
+ BASENAME=$(basename "$IMPL_PATH")
137
+
138
+ # Strip extension to get stem (e.g., "Hero" from "Hero.tsx")
139
+ # Handle .ts, .tsx, .js, .jsx
140
+ case "$BASENAME" in
141
+ *.tsx) STEM="${BASENAME%.tsx}"; EXT="tsx" ;;
142
+ *.ts) STEM="${BASENAME%.ts}"; EXT="ts" ;;
143
+ *.jsx) STEM="${BASENAME%.jsx}"; EXT="jsx" ;;
144
+ *.js) STEM="${BASENAME%.js}"; EXT="js" ;;
145
+ *) STEM="$BASENAME"; EXT="" ;;
146
+ esac
147
+
148
+ local TEST_FILES
149
+ TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
150
+ if [ -z "$TEST_FILES" ]; then
151
+ echo ""
152
+ return
153
+ fi
154
+
155
+ # Check tracked test files for a match
156
+ # Priority: exact match in tracked files (any convention)
157
+ while IFS= read -r tracked; do
158
+ local tracked_dir tracked_base
159
+ tracked_dir=$(dirname "$tracked")
160
+ tracked_base=$(basename "$tracked")
161
+
162
+ # Same directory: Hero.test.tsx, Hero.spec.tsx, etc.
163
+ if [ "$tracked_dir" = "$DIR" ]; then
164
+ case "$tracked_base" in
165
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
166
+ esac
167
+ fi
168
+
169
+ # __tests__ subdirectory: src/__tests__/Hero.test.tsx for src/Hero.tsx
170
+ if [ "$tracked_dir" = "${DIR}/__tests__" ]; then
171
+ case "$tracked_base" in
172
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
173
+ esac
174
+ fi
175
+
176
+ # Parent __tests__: src/__tests__/Hero.test.tsx for src/components/Hero.tsx
177
+ # (only if dir contains __tests__)
178
+ case "$tracked" in
179
+ */__tests__/*)
180
+ case "$tracked_base" in
181
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
182
+ esac
183
+ ;;
184
+ esac
185
+ done <<< "$TEST_FILES"
186
+
187
+ # No tracked test found
188
+ echo ""
189
+ }
190
+
191
+ # Suggest a test file path for an impl file using naming convention.
192
+ # Used for user-facing messages (e.g., "write src/Hero.test.tsx first").
193
+ tdd_suggest_test_path() {
194
+ local IMPL_PATH="$1"
195
+ local DIR BASENAME STEM EXT
196
+ DIR=$(dirname "$IMPL_PATH")
197
+ BASENAME=$(basename "$IMPL_PATH")
198
+ case "$BASENAME" in
199
+ *.tsx) STEM="${BASENAME%.tsx}"; EXT="tsx" ;;
200
+ *.ts) STEM="${BASENAME%.ts}"; EXT="ts" ;;
201
+ *.jsx) STEM="${BASENAME%.jsx}"; EXT="jsx" ;;
202
+ *.js) STEM="${BASENAME%.js}"; EXT="js" ;;
203
+ *) echo ""; return ;;
204
+ esac
205
+ echo "${DIR}/${STEM}.test.${EXT}"
206
+ }
207
+
208
+ # Read state for an implementation file by looking up its associated test
209
+ tdd_read_state_for_impl() {
210
+ local SESSION_ID="$1"
211
+ local IMPL_PATH="$2"
212
+ local TEST_FILE
213
+ TEST_FILE=$(tdd_find_test_for_impl "$SESSION_ID" "$IMPL_PATH")
214
+ if [ -z "$TEST_FILE" ]; then
215
+ echo "IDLE"
216
+ return
217
+ fi
218
+ tdd_read_state "$SESSION_ID" "$TEST_FILE"
219
+ }
220
+
221
+ # Get all tracked test files with their states (format: "path:STATE" per line)
222
+ tdd_get_all_states() {
223
+ local SESSION_ID="$1"
224
+ local TEST_FILES
225
+ TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
226
+ if [ -z "$TEST_FILES" ]; then
227
+ return
228
+ fi
229
+ while IFS= read -r test_file; do
230
+ local state
231
+ state=$(tdd_read_state "$SESSION_ID" "$test_file")
232
+ echo "${test_file}:${state}"
233
+ done <<< "$TEST_FILES"
234
+ }
235
+
111
236
  # --- Test Runner ---
112
- # Runs tests for the session's tracked test files.
237
+ # Runs a single test file.
113
238
  # Returns: 0=pass, 1=fail, 124=timeout, other=error
114
239
  # Saves stdout to marker file for debugging.
115
- tdd_run_tests() {
240
+ tdd_run_test_file() {
116
241
  local SESSION_ID="$1"
242
+ local TEST_FILE="$2"
117
243
  local STDOUT_FILE
118
244
  STDOUT_FILE=$(_tdd_test_stdout_file "$SESSION_ID")
119
- local TEST_FILES
120
- TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
121
245
 
122
- if [ -z "$TEST_FILES" ]; then
123
- echo "No test files tracked" > "$STDOUT_FILE"
124
- return 0 # No tests to run = pass (no-op)
246
+ if [ -z "$TEST_FILE" ]; then
247
+ echo "No test file specified" > "$STDOUT_FILE"
248
+ return 0
125
249
  fi
126
250
 
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
251
+ # Run with timeout only the specified test file
134
252
  local EXIT_CODE
135
- timeout "$TDD_TEST_TIMEOUT" bash -c "$TDD_TEST_CMD $ARGS" > "$STDOUT_FILE" 2>&1
253
+ timeout "$TDD_TEST_TIMEOUT" bash -c "$TDD_TEST_CMD $TEST_FILE" > "$STDOUT_FILE" 2>&1
136
254
  EXIT_CODE=$?
137
255
 
138
256
  return $EXIT_CODE
@@ -155,9 +273,12 @@ tdd_has_test_script() {
155
273
  # --- Cleanup ---
156
274
  tdd_cleanup() {
157
275
  local SESSION_ID="$1"
158
- rm -f "$(_tdd_state_file "$SESSION_ID")"
276
+ local STATE_DIR
277
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
278
+ rm -rf "$STATE_DIR"
159
279
  rm -f "$(_tdd_test_files_file "$SESSION_ID")"
160
280
  rm -f "$(_tdd_test_stdout_file "$SESSION_ID")"
281
+ rm -f "/tmp/tdd-setup-active-${SESSION_ID}"
161
282
  }
162
283
 
163
284
  # --- Deny Helper ---
@@ -1,7 +1,8 @@
1
1
  #!/bin/bash
2
2
  # TDD - PreToolUse enforcement hook (Edit|Write)
3
- # Blocks implementation file edits unless state is RED or GREEN.
3
+ # Blocks implementation file edits unless the associated test's state is RED or GREEN.
4
4
  # Test files and exempt files are always allowed.
5
+ # Per-file state: a failing Countdown test does NOT block editing Hero.
5
6
 
6
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
8
  source "$SCRIPT_DIR/lib/tdd-gate.sh"
@@ -15,16 +16,30 @@ if [ -z "$FILE_PATH" ]; then
15
16
  exit 0
16
17
  fi
17
18
 
19
+ # P004: Only gate files inside the project root.
20
+ case "$FILE_PATH" in
21
+ /*)
22
+ case "$FILE_PATH" in
23
+ "$PWD"/*) ;;
24
+ *) exit 0 ;;
25
+ esac
26
+ ;;
27
+ esac
28
+
18
29
  # Classify first, then check test script (only impl files need gating)
19
30
  FILE_TYPE=$(tdd_classify_file "$FILE_PATH")
20
31
  if [ "$FILE_TYPE" != "impl" ]; then
21
32
  exit 0
22
33
  fi
23
34
 
24
- # If no test script configured, block and direct to create skill
35
+ # If no test script configured, check if setup skill is running
25
36
  if ! tdd_has_test_script; then
37
+ # Allow edits if the setup skill is actively running (chicken-and-egg bypass)
38
+ if [ -n "$SESSION_ID" ] && [ -f "/tmp/tdd-setup-active-${SESSION_ID}" ]; then
39
+ exit 0
40
+ fi
26
41
  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."
42
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' because no test script is configured in package.json. Run /wr-tdd:setup-tests to set up a test framework for this project. TDD enforcement requires a working test runner before implementation code can be written."
28
43
  exit 0
29
44
  fi
30
45
 
@@ -34,25 +49,26 @@ if [ -z "$SESSION_ID" ]; then
34
49
  exit 0
35
50
  fi
36
51
 
37
- # Check state for implementation files
38
- STATE=$(tdd_read_state "$SESSION_ID")
52
+ # Check state for THIS implementation file's associated test
53
+ STATE=$(tdd_read_state_for_impl "$SESSION_ID" "$FILE_PATH")
39
54
  BASENAME=$(basename "$FILE_PATH")
55
+ SUGGESTED_TEST=$(tdd_suggest_test_path "$FILE_PATH")
40
56
 
41
57
  case "$STATE" in
42
58
  RED|GREEN)
43
- # Allowed: agent is in the TDD cycle
59
+ # Allowed: this file's test is in the TDD cycle
44
60
  exit 0
45
61
  ;;
46
62
  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."
63
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- no tests written for this file yet. TDD state is IDLE. Write a failing test first (e.g., ${SUGGESTED_TEST}) before editing this implementation file."
48
64
  exit 0
49
65
  ;;
50
66
  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)."
67
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- its test runner timed out. TDD state is BLOCKED. Fix the test setup for this file before continuing."
52
68
  exit 0
53
69
  ;;
54
70
  *)
55
- tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- unknown TDD state '${STATE}'. Write a failing test first."
71
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- unknown TDD state '${STATE}'. Write a failing test first (e.g., ${SUGGESTED_TEST})."
56
72
  exit 0
57
73
  ;;
58
74
  esac
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # TDD - UserPromptSubmit hook
3
- # Injects TDD instructions and current state into every prompt.
3
+ # Injects TDD instructions and per-file state into every prompt.
4
4
  # Only active when a test script is configured in package.json.
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -18,56 +18,67 @@ This project has NO test script configured in package.json. Implementation file
18
18
  edits (.ts, .tsx, .js, .jsx) are BLOCKED until testing is set up.
19
19
 
20
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.
21
+ run /wr-tdd:setup-tests first to configure a test framework for this project.
22
22
 
23
23
  Test files, config files, docs, and styles are still writable.
24
24
  HOOK_OUTPUT
25
25
  exit 0
26
26
  fi
27
27
 
28
- STATE="IDLE"
28
+ # Collect per-file states
29
+ ALL_STATES=""
29
30
  if [ -n "$SESSION_ID" ]; then
30
- STATE=$(tdd_read_state "$SESSION_ID")
31
+ ALL_STATES=$(tdd_get_all_states "$SESSION_ID")
31
32
  fi
32
33
 
33
- TEST_FILES=""
34
- if [ -n "$SESSION_ID" ]; then
35
- TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
34
+ # Determine overall status for the header
35
+ OVERALL="IDLE"
36
+ if [ -n "$ALL_STATES" ]; then
37
+ if echo "$ALL_STATES" | grep -q ":BLOCKED$"; then
38
+ OVERALL="BLOCKED (some tests)"
39
+ elif echo "$ALL_STATES" | grep -q ":RED$"; then
40
+ OVERALL="RED (some tests failing)"
41
+ elif echo "$ALL_STATES" | grep -q ":GREEN$"; then
42
+ OVERALL="GREEN"
43
+ fi
36
44
  fi
37
45
 
38
46
  cat <<HOOK_OUTPUT
39
47
  INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
40
48
 
41
- This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${STATE}**
49
+ This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${OVERALL}**
42
50
 
43
- STATE RULES:
51
+ STATE RULES (per test file — each component has independent state):
44
52
  - 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.
53
+ Implementation file edits (.ts, .tsx, .js, .jsx) are BLOCKED until you write a test for that file.
46
54
  - RED: Tests are failing. Write implementation code to make them pass.
47
- Implementation file edits are ALLOWED.
55
+ Implementation file edits are ALLOWED for files whose associated test is RED.
48
56
  - 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.
57
+ Implementation file edits are ALLOWED for files whose associated test is GREEN.
58
+ - BLOCKED: Test runner timed out. Fix the test setup before continuing.
59
+ Implementation file edits are BLOCKED for files whose associated test is BLOCKED.
52
60
 
53
61
  WORKFLOW:
54
- 1. Write a test file (*.test.ts, *.spec.ts, etc.) that describes the desired behavior
62
+ 1. Write a test file (*.test.ts or *.spec.ts) that describes the desired behavior
55
63
  2. The test MUST fail (RED state) -- this proves the test is meaningful
56
64
  3. Write the minimum implementation to make the test pass (GREEN state)
57
65
  4. Refactor while keeping tests green
58
66
  5. Repeat for the next behavior
59
67
 
60
68
  IMPORTANT:
69
+ - State is tracked PER TEST FILE — a failing Countdown test does NOT block editing Hero
61
70
  - 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
71
+ - Implementation files are ONLY writable when their associated test is RED or GREEN
72
+ - The hook runs only the relevant test after each file write (not the full suite)
64
73
  - To refactor existing code, touch the relevant test file first to enter the cycle
65
74
  HOOK_OUTPUT
66
75
 
67
- if [ -n "$TEST_FILES" ]; then
76
+ if [ -n "$ALL_STATES" ]; then
68
77
  echo ""
69
78
  echo "TRACKED TEST FILES THIS SESSION:"
70
- echo "$TEST_FILES" | sed 's/^/ - /'
79
+ echo "$ALL_STATES" | while IFS=: read -r file state; do
80
+ echo " - ${file} [${state}]"
81
+ done
71
82
  fi
72
83
 
73
84
  exit 0
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # TDD - PostToolUse hook (Edit|Write)
3
- # Runs tests after file writes and transitions state.
3
+ # Runs only the relevant test after file writes and transitions per-file state.
4
4
  # Emits additionalContext with the current TDD state.
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -28,45 +28,49 @@ if [ "$FILE_TYPE" = "exempt" ]; then
28
28
  exit 0
29
29
  fi
30
30
 
31
- # Track test files
31
+ # Determine which test file to run
32
+ TEST_FILE=""
33
+
32
34
  if [ "$FILE_TYPE" = "test" ]; then
35
+ # Written a test file — track it and run it
33
36
  tdd_add_test_file "$SESSION_ID" "$FILE_PATH"
37
+ TEST_FILE="$FILE_PATH"
38
+ elif [ "$FILE_TYPE" = "impl" ]; then
39
+ # Written an impl file — find and run its associated test
40
+ TEST_FILE=$(tdd_find_test_for_impl "$SESSION_ID" "$FILE_PATH")
41
+ fi
42
+
43
+ # If no test file to run, nothing to do
44
+ if [ -z "$TEST_FILE" ]; then
45
+ exit 0
34
46
  fi
35
47
 
36
- # Run tests
37
- tdd_run_tests "$SESSION_ID"
48
+ # Run only the relevant test file
49
+ tdd_run_test_file "$SESSION_ID" "$TEST_FILE"
38
50
  TEST_EXIT=$?
39
51
 
40
- # Read current state
41
- OLD_STATE=$(tdd_read_state "$SESSION_ID")
52
+ # Read current state for this specific test file
53
+ OLD_STATE=$(tdd_read_state "$SESSION_ID" "$TEST_FILE")
42
54
 
43
- # Transition state based on file type + test result
55
+ # Transition state based on test result
56
+ # Only timeout (124) → BLOCKED. All other failures → RED.
44
57
  NEW_STATE="$OLD_STATE"
45
58
 
46
59
  if [ $TEST_EXIT -eq 0 ]; then
47
60
  # Tests pass
48
61
  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
62
+ elif [ $TEST_EXIT -eq 124 ]; then
63
+ # Timeout — genuinely broken setup
65
64
  NEW_STATE="BLOCKED"
65
+ else
66
+ # Tests fail (exit code 1, 2, or any other non-zero)
67
+ # This includes import errors, syntax errors, assertion failures.
68
+ # All of these are RED — the user should write impl to fix them.
69
+ NEW_STATE="RED"
66
70
  fi
67
71
 
68
- # Write new state
69
- tdd_write_state "$SESSION_ID" "$NEW_STATE"
72
+ # Write new state for this specific test file
73
+ tdd_write_state "$SESSION_ID" "$TEST_FILE" "$NEW_STATE"
70
74
 
71
75
  # Read last test output for context
72
76
  STDOUT_FILE="/tmp/tdd-test-stdout-${SESSION_ID}"
@@ -85,7 +89,8 @@ fi
85
89
 
86
90
  cat <<EOF
87
91
  TDD STATE UPDATE: ${TRANSITION}
88
- Current state: ${NEW_STATE}
92
+ Test file: ${TEST_FILE}
93
+ State: ${NEW_STATE}
89
94
  File written: ${FILE_PATH} (${FILE_TYPE})
90
95
  Test result: exit code ${TEST_EXIT}
91
96
  EOF
@@ -99,15 +104,15 @@ fi
99
104
  case "$NEW_STATE" in
100
105
  RED)
101
106
  echo ""
102
- echo "ACTION: Tests are failing. Write implementation code to make them pass."
107
+ echo "ACTION: Tests are failing for ${TEST_FILE}. Write implementation code to make them pass."
103
108
  ;;
104
109
  GREEN)
105
110
  echo ""
106
- echo "ACTION: Tests are passing. You may refactor or write a new failing test for the next behavior."
111
+ echo "ACTION: Tests are passing for ${TEST_FILE}. You may refactor or write a new failing test for the next behavior."
107
112
  ;;
108
113
  BLOCKED)
109
114
  echo ""
110
- echo "ACTION: Test runner error (exit code ${TEST_EXIT}). Fix the test setup before continuing."
115
+ echo "ACTION: Test runner timed out for ${TEST_FILE} (exit code ${TEST_EXIT}). Fix the test setup before continuing."
111
116
  ;;
112
117
  esac
113
118
 
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # PostToolUse:Skill hook — sets a marker when /wr-tdd:setup-tests is invoked.
3
+ # This marker allows tdd-enforce-edit.sh to permit edits during test setup,
4
+ # avoiding the chicken-and-egg problem where the setup skill needs to write
5
+ # .ts/.js files but the enforce hook blocks them.
6
+
7
+ INPUT=$(cat)
8
+
9
+ SKILL_NAME=$(echo "$INPUT" | jq -r '.tool_input.skill // empty') || true
10
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
11
+
12
+ if [ -z "$SESSION_ID" ] || [ -z "$SKILL_NAME" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Match the TDD setup skill (handles both full and short names)
17
+ case "$SKILL_NAME" in
18
+ *tdd*setup*|*setup*test*)
19
+ touch "/tmp/tdd-setup-active-${SESSION_ID}"
20
+ ;;
21
+ esac
22
+
23
+ exit 0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Tests for tdd-gate.sh shared library functions
4
+
5
+ setup() {
6
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
7
+ source "$SCRIPT_DIR/lib/tdd-gate.sh"
8
+ TEST_SESSION="test-$$-$BATS_TEST_NUMBER"
9
+ }
10
+
11
+ teardown() {
12
+ tdd_cleanup "$TEST_SESSION" 2>/dev/null || true
13
+ rm -f "/tmp/tdd-setup-active-${TEST_SESSION}"
14
+ }
15
+
16
+ # --- tdd_classify_file ---
17
+
18
+ @test "classify_file: .test.ts is test" {
19
+ result=$(tdd_classify_file "src/utils.test.ts")
20
+ [ "$result" = "test" ]
21
+ }
22
+
23
+ @test "classify_file: .spec.jsx is test" {
24
+ result=$(tdd_classify_file "src/App.spec.jsx")
25
+ [ "$result" = "test" ]
26
+ }
27
+
28
+ @test "classify_file: __tests__ directory is test" {
29
+ result=$(tdd_classify_file "src/__tests__/utils.ts")
30
+ [ "$result" = "test" ]
31
+ }
32
+
33
+ @test "classify_file: .config.ts is exempt" {
34
+ result=$(tdd_classify_file "vitest.config.ts")
35
+ [ "$result" = "exempt" ]
36
+ }
37
+
38
+ @test "classify_file: .setup.ts is exempt" {
39
+ result=$(tdd_classify_file "vitest.setup.ts")
40
+ [ "$result" = "exempt" ]
41
+ }
42
+
43
+ @test "classify_file: .json is exempt" {
44
+ result=$(tdd_classify_file "package.json")
45
+ [ "$result" = "exempt" ]
46
+ }
47
+
48
+ @test "classify_file: .css is exempt" {
49
+ result=$(tdd_classify_file "src/styles.css")
50
+ [ "$result" = "exempt" ]
51
+ }
52
+
53
+ @test "classify_file: .md is exempt" {
54
+ result=$(tdd_classify_file "README.md")
55
+ [ "$result" = "exempt" ]
56
+ }
57
+
58
+ @test "classify_file: .sh is exempt" {
59
+ result=$(tdd_classify_file "scripts/build.sh")
60
+ [ "$result" = "exempt" ]
61
+ }
62
+
63
+ @test "classify_file: .ts is impl" {
64
+ result=$(tdd_classify_file "src/utils.ts")
65
+ [ "$result" = "impl" ]
66
+ }
67
+
68
+ @test "classify_file: .tsx is impl" {
69
+ result=$(tdd_classify_file "src/App.tsx")
70
+ [ "$result" = "impl" ]
71
+ }
72
+
73
+ @test "classify_file: .js is impl" {
74
+ result=$(tdd_classify_file "src/index.js")
75
+ [ "$result" = "impl" ]
76
+ }
77
+
78
+ @test "classify_file: unknown extension is exempt" {
79
+ result=$(tdd_classify_file "Dockerfile")
80
+ [ "$result" = "exempt" ]
81
+ }
82
+
83
+ # --- tdd_has_test_script ---
84
+
85
+ @test "has_test_script: returns false when no package.json" {
86
+ cd "$(mktemp -d)"
87
+ run tdd_has_test_script
88
+ [ "$status" -ne 0 ]
89
+ }
90
+
91
+ @test "has_test_script: returns false for default npm test script" {
92
+ local tmpdir=$(mktemp -d)
93
+ echo '{"scripts":{"test":"echo \"Error: no test specified\" && exit 1"}}' > "$tmpdir/package.json"
94
+ cd "$tmpdir"
95
+ run tdd_has_test_script
96
+ [ "$status" -ne 0 ]
97
+ rm -rf "$tmpdir"
98
+ }
99
+
100
+ @test "has_test_script: returns true for real test script" {
101
+ local tmpdir=$(mktemp -d)
102
+ echo '{"scripts":{"test":"vitest"}}' > "$tmpdir/package.json"
103
+ cd "$tmpdir"
104
+ run tdd_has_test_script
105
+ [ "$status" -eq 0 ]
106
+ rm -rf "$tmpdir"
107
+ }
108
+
109
+ # --- Per-test-file state ---
110
+
111
+ @test "read_state: returns IDLE when no state exists for test file" {
112
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
113
+ [ "$result" = "IDLE" ]
114
+ }
115
+
116
+ @test "write_state and read_state roundtrip for a specific test file" {
117
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
118
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
119
+ [ "$result" = "RED" ]
120
+ }
121
+
122
+ @test "per-file state: different test files have independent states" {
123
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
124
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "RED"
125
+
126
+ hero_state=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
127
+ countdown_state=$(tdd_read_state "$TEST_SESSION" "src/Countdown.test.tsx")
128
+
129
+ [ "$hero_state" = "GREEN" ]
130
+ [ "$countdown_state" = "RED" ]
131
+ }
132
+
133
+ @test "per-file state: BLOCKED on one test does not affect another" {
134
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
135
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "BLOCKED"
136
+
137
+ hero_state=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
138
+ [ "$hero_state" = "GREEN" ]
139
+ }
140
+
141
+ @test "cleanup removes all per-file state" {
142
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
143
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "GREEN"
144
+ tdd_cleanup "$TEST_SESSION"
145
+
146
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
147
+ [ "$result" = "IDLE" ]
148
+ result=$(tdd_read_state "$TEST_SESSION" "src/Countdown.test.tsx")
149
+ [ "$result" = "IDLE" ]
150
+ }
151
+
152
+ @test "cleanup removes setup marker" {
153
+ touch "/tmp/tdd-setup-active-${TEST_SESSION}"
154
+ tdd_cleanup "$TEST_SESSION"
155
+ [ ! -f "/tmp/tdd-setup-active-${TEST_SESSION}" ]
156
+ }
157
+
158
+ # --- Test file tracking ---
159
+
160
+ @test "add_test_file and get_test_files roundtrip" {
161
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
162
+ result=$(tdd_get_test_files "$TEST_SESSION")
163
+ [ "$result" = "src/Hero.test.tsx" ]
164
+ }
165
+
166
+ @test "add_test_file deduplicates" {
167
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
168
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
169
+ local count
170
+ count=$(tdd_get_test_files "$TEST_SESSION" | wc -l | tr -d ' ')
171
+ [ "$count" -eq 1 ]
172
+ }
173
+
174
+ # --- Impl-to-test association ---
175
+
176
+ @test "find_test_for_impl: Hero.tsx associates with tracked Hero.test.tsx" {
177
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
178
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
179
+ [ "$result" = "src/Hero.test.tsx" ]
180
+ }
181
+
182
+ @test "find_test_for_impl: utils.ts associates with tracked utils.test.ts" {
183
+ tdd_add_test_file "$TEST_SESSION" "src/utils.test.ts"
184
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/utils.ts")
185
+ [ "$result" = "src/utils.test.ts" ]
186
+ }
187
+
188
+ @test "suggest_test_path: Hero.tsx suggests Hero.test.tsx" {
189
+ result=$(tdd_suggest_test_path "src/Hero.tsx")
190
+ [ "$result" = "src/Hero.test.tsx" ]
191
+ }
192
+
193
+ @test "suggest_test_path: utils.ts suggests utils.test.ts" {
194
+ result=$(tdd_suggest_test_path "src/utils.ts")
195
+ [ "$result" = "src/utils.test.ts" ]
196
+ }
197
+
198
+ @test "find_test_for_impl: prefers tracked .test over .spec" {
199
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.spec.tsx"
200
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
201
+ [ "$result" = "src/Hero.spec.tsx" ]
202
+ }
203
+
204
+ @test "find_test_for_impl: finds __tests__ variant when tracked" {
205
+ tdd_add_test_file "$TEST_SESSION" "src/__tests__/Hero.test.tsx"
206
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
207
+ [ "$result" = "src/__tests__/Hero.test.tsx" ]
208
+ }
209
+
210
+ @test "find_test_for_impl: returns empty when no test tracked" {
211
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Unknown.tsx")
212
+ [ -z "$result" ]
213
+ }
214
+
215
+ # --- State for impl files (via association) ---
216
+
217
+ @test "read_state_for_impl: returns IDLE when no associated test" {
218
+ result=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
219
+ [ "$result" = "IDLE" ]
220
+ }
221
+
222
+ @test "read_state_for_impl: returns associated test's state" {
223
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
224
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
225
+ result=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
226
+ [ "$result" = "RED" ]
227
+ }
228
+
229
+ @test "read_state_for_impl: Hero GREEN while Countdown BLOCKED" {
230
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
231
+ tdd_add_test_file "$TEST_SESSION" "src/Countdown.test.tsx"
232
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
233
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "BLOCKED"
234
+
235
+ hero=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
236
+ countdown=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Countdown.tsx")
237
+
238
+ [ "$hero" = "GREEN" ]
239
+ [ "$countdown" = "BLOCKED" ]
240
+ }
241
+
242
+ # --- get_all_states ---
243
+
244
+ @test "get_all_states: lists all test files with their states" {
245
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
246
+ tdd_add_test_file "$TEST_SESSION" "src/Countdown.test.tsx"
247
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
248
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "RED"
249
+
250
+ result=$(tdd_get_all_states "$TEST_SESSION")
251
+ echo "$result" | grep -q "src/Hero.test.tsx:GREEN"
252
+ echo "$result" | grep -q "src/Countdown.test.tsx:RED"
253
+ }
254
+
255
+ # --- Scoped test runner ---
256
+
257
+ @test "run_test_file: runs only the specified test file" {
258
+ local tmpdir=$(mktemp -d)
259
+ cd "$tmpdir"
260
+
261
+ TDD_TEST_CMD="echo"
262
+ tdd_run_test_file "$TEST_SESSION" "src/Hero.test.tsx"
263
+ local exit_code=$?
264
+ [ "$exit_code" -eq 0 ]
265
+
266
+ # The stdout file should contain just the one file path (from echo)
267
+ local stdout_file="/tmp/tdd-test-stdout-${TEST_SESSION}"
268
+ [ -f "$stdout_file" ]
269
+ grep -q "src/Hero.test.tsx" "$stdout_file"
270
+ # Should NOT contain any other test file
271
+ local line_count
272
+ line_count=$(wc -l < "$stdout_file" | tr -d ' ')
273
+ [ "$line_count" -eq 1 ]
274
+
275
+ rm -rf "$tmpdir"
276
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P004: tdd-enforce-edit.sh project-root check.
4
+
5
+ setup() {
6
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
7
+ HOOK="$SCRIPT_DIR/tdd-enforce-edit.sh"
8
+ }
9
+
10
+ run_hook_with_file() {
11
+ local file_path="$1"
12
+ local json="{\"tool_input\":{\"file_path\":\"${file_path}\"},\"session_id\":\"test-$$\"}"
13
+ echo "$json" | bash "$HOOK"
14
+ }
15
+
16
+ @test "tdd project-root: absolute .ts outside project exits 0" {
17
+ run run_hook_with_file "/Users/other/project/src/foo.ts"
18
+ [ "$status" -eq 0 ]
19
+ [[ "$output" != *"BLOCKED"* ]]
20
+ }
@@ -0,0 +1,143 @@
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
+
51
+ export function addMarketplace() {
52
+ return run(
53
+ `claude plugin marketplace add ${MARKETPLACE_REPO}`,
54
+ `Marketplace: ${MARKETPLACE_NAME}`
55
+ );
56
+ }
57
+
58
+ export function installPlugin(pluginName, { scope = "project" } = {}) {
59
+ return run(
60
+ `claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
61
+ pluginName
62
+ );
63
+ }
64
+
65
+ export function updatePlugin(pluginName) {
66
+ return run(`claude plugin update ${pluginName}`, pluginName);
67
+ }
68
+
69
+ export function uninstallPlugin(pluginName) {
70
+ return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
71
+ }
72
+
73
+ /**
74
+ * Install a single package: marketplace add + plugin install.
75
+ */
76
+ export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
77
+ console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
78
+
79
+ addMarketplace();
80
+ installPlugin(pluginName, { scope });
81
+
82
+ if (deps.length > 0) {
83
+ console.log(`\nNote: This plugin works best with:`);
84
+ for (const dep of deps) {
85
+ console.log(` - @windyroad/${dep.replace("wr-", "")} (npx @windyroad/${dep.replace("wr-", "")})`);
86
+ }
87
+ }
88
+
89
+ console.log(
90
+ `\nDone! Restart Claude Code to activate.\n`
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Update a single package.
96
+ */
97
+ export function updatePackage(pluginName) {
98
+ console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
99
+
100
+ run(
101
+ `claude plugin marketplace update ${MARKETPLACE_NAME}`,
102
+ "Updating marketplace"
103
+ );
104
+ updatePlugin(pluginName);
105
+
106
+ console.log("\nDone! Restart Claude Code to apply updates.\n");
107
+ }
108
+
109
+ /**
110
+ * Uninstall a single package.
111
+ */
112
+ export function uninstallPackage(pluginName) {
113
+ console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
114
+
115
+ uninstallPlugin(pluginName);
116
+
117
+ console.log("\nDone. Restart Claude Code to apply changes.\n");
118
+ }
119
+
120
+ /**
121
+ * Parse standard flags used by all per-plugin installers.
122
+ */
123
+ export function parseStandardArgs(argv) {
124
+ const args = argv.slice(2);
125
+ const flags = {
126
+ help: args.includes("--help") || args.includes("-h"),
127
+ uninstall: args.includes("--uninstall"),
128
+ update: args.includes("--update"),
129
+ dryRun: args.includes("--dry-run"),
130
+ scope: "project",
131
+ };
132
+ const scopeIdx = args.indexOf("--scope");
133
+ if (scopeIdx !== -1 && args[scopeIdx + 1]) {
134
+ const val = args[scopeIdx + 1];
135
+ if (["project", "user", "local"].includes(val)) {
136
+ flags.scope = val;
137
+ } else {
138
+ console.error("--scope requires: project, user, or local");
139
+ process.exit(1);
140
+ }
141
+ }
142
+ return flags;
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/tdd",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
5
5
  "bin": {
6
6
  "windyroad-tdd": "./bin/install.mjs"
@@ -23,6 +23,7 @@
23
23
  "agents/",
24
24
  "hooks/",
25
25
  "skills/",
26
- ".claude-plugin/"
26
+ ".claude-plugin/",
27
+ "lib/"
27
28
  ]
28
29
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: wr:tdd
2
+ name: wr-tdd:setup-tests
3
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
4
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
5
5
  ---