diff --git a/README.md b/README.md index 6bc7f70..09cdde0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,10 @@ Once restarted, you'll see a confirmation message and notifications will appear - [Warp terminal](https://warp.dev) (macOS, Linux, or Windows) - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI -- `jq` for JSON parsing (install via `brew install jq` or your package manager) +- `jq` for JSON parsing — install via your package manager: + - macOS: `brew install jq` + - Linux: `sudo apt install jq` (Debian/Ubuntu) or `sudo dnf install jq` (Fedora) + - Windows: `scoop install jq` or `choco install jq` ## How It Works diff --git a/plugins/warp/scripts/build-payload.sh b/plugins/warp/scripts/build-payload.sh index 9ad610e..b2414e1 100644 --- a/plugins/warp/scripts/build-payload.sh +++ b/plugins/warp/scripts/build-payload.sh @@ -13,6 +13,14 @@ # The function extracts common fields (session_id, cwd, project) from the # hook's stdin JSON (passed as $1), then merges any extra jq args you pass. +# Bail out early if jq is not available — every path through this script +# requires it, and a missing jq produces confusing "command not found" errors +# in hook output that Claude Code surfaces as hook errors. +if ! command -v jq &>/dev/null; then + echo "jq is required but not found. Install it via your package manager." >&2 + exit 1 +fi + # The current protocol version this plugin knows how to produce. PLUGIN_CURRENT_PROTOCOL_VERSION=1 diff --git a/plugins/warp/scripts/emit-terminal-sequence.sh b/plugins/warp/scripts/emit-terminal-sequence.sh index b6a83b9..604a2d7 100644 --- a/plugins/warp/scripts/emit-terminal-sequence.sh +++ b/plugins/warp/scripts/emit-terminal-sequence.sh @@ -8,9 +8,10 @@ # write to /dev/tty instead. # # Decision tree: -# 1. CLAUDE_CODE_VERSION known, >= 2.1.141 → emit terminalSequence JSON -# 2. CLAUDE_CODE_VERSION known, < 2.1.141 → write /dev/tty; give up if missing -# 3. CLAUDE_CODE_VERSION unknown → try /dev/tty, fall back to JSON +# 1. Windows (MSYS/MINGW/Cygwin) → always use terminalSequence JSON +# 2. CLAUDE_CODE_VERSION known, >= 2.1.141 → emit terminalSequence JSON +# 3. CLAUDE_CODE_VERSION known, < 2.1.141 → write /dev/tty; give up if missing +# 4. CLAUDE_CODE_VERSION unknown (non-Windows) → try /dev/tty, fall back to JSON # # Usage: # source "$SCRIPT_DIR/emit-terminal-sequence.sh" @@ -24,6 +25,13 @@ # The first Claude Code version that supports the terminalSequence output field. TERMINAL_SEQUENCE_MIN_VERSION="2.1.141" +# Returns 0 (true) if running under Windows (MSYS, MINGW, or Cygwin). +_is_windows() { + local kernel + kernel="$(uname -s 2>/dev/null)" + [[ "$kernel" == MINGW* || "$kernel" == MSYS* || "$kernel" == CYGWIN* ]] +} + # Compare two dotted version strings (e.g. "2.1.141" >= "2.1.141"). # Returns 0 (true) if $1 >= $2, 1 (false) otherwise. _version_at_least() { @@ -59,6 +67,12 @@ emit_terminal_sequence() { local seq="$1" [ -z "$seq" ] && return 0 + # Windows has no /dev/tty — always use the JSON terminalSequence path. + if _is_windows; then + jq -nc --arg seq "$seq" '{terminalSequence: $seq}' + return 0 + fi + # Classify the running Claude Code version, if we can. local raw="${CLAUDE_CODE_VERSION:-}" local ver="" diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 754bdd0..0ce89b7 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -260,6 +260,58 @@ OUTPUT=$(emit_terminal_sequence "test-seq") assert_json_field "new CC outputs terminalSequence" "$OUTPUT" ".terminalSequence" "test-seq" unset CLAUDE_CODE_VERSION +echo "" +echo "--- _is_windows ---" + +# _is_windows should return 0 on Windows (MSYS/MINGW/Cygwin), 1 otherwise. +# The test result depends on the host OS, so we assert against uname. +_kernel="$(uname -s 2>/dev/null)" +_is_windows +if [[ "$_kernel" == MINGW* || "$_kernel" == MSYS* || "$_kernel" == CYGWIN* ]]; then + assert_eq "Windows kernel returns true" "0" "$?" +else + assert_eq "non-Windows kernel returns false" "1" "$?" +fi + +echo "" +echo "--- Windows: emit_terminal_sequence skips /dev/tty ---" + +# On non-Windows we can't fully test the Windows branch, but we can verify +# that when _is_windows is true (mocked), the function outputs terminalSequence JSON +# instead of trying /dev/tty. We mock _is_windows by temporarily overriding it. +_is_windows_orig="$(declare -f _is_windows)" +_is_windows_mock() { return 0; } +# Replace the function +eval "_is_windows() { return 0; }" + +unset CLAUDE_CODE_VERSION +OUTPUT=$(emit_terminal_sequence "win-test-seq" 2>&1) +assert_json_field "Windows outputs terminalSequence JSON" "$OUTPUT" ".terminalSequence" "win-test-seq" + +# Also test with old CC version on Windows — should still use JSON, not /dev/tty +export CLAUDE_CODE_VERSION="2.1.100" +OUTPUT=$(emit_terminal_sequence "win-old-cc-seq" 2>&1) +assert_json_field "Windows + old CC outputs terminalSequence JSON" "$OUTPUT" ".terminalSequence" "win-old-cc-seq" +unset CLAUDE_CODE_VERSION + +# Restore original _is_windows +eval "$_is_windows_orig" + +echo "" +echo "--- jq guard in build-payload.sh ---" + +# When jq is missing, sourcing build-payload.sh should exit 1 with a clear message. +# We set PATH to only include the bash binary's directory (no jq). +SCRIPT_DIR_TEST="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)" +_minimal_path="$(dirname "$(command -v bash)")" +OUTPUT=$(PATH="$_minimal_path" bash -c "source '$SCRIPT_DIR_TEST/build-payload.sh'" 2>&1) +_rc=$? +if [ "$_rc" -ne 0 ] && echo "$OUTPUT" | grep -qi "jq"; then + assert_eq "build-payload.sh fails gracefully when jq missing" "0" "0" +else + assert_eq "build-payload.sh fails gracefully when jq missing" "1" "0 (exit=$_rc, output=$OUTPUT)" +fi + # --- Routing tests --- # These test the hook scripts as subprocesses to verify routing behavior. # We override /dev/tty writes since they'd fail in CI.