From aa8c9338cb8065762ef74b102f7f26cf2ad5d739 Mon Sep 17 00:00:00 2001 From: hypn4 Date: Wed, 13 May 2026 23:47:16 +0900 Subject: [PATCH 1/2] fix(scripts): normalise MinGW/MSYS/Cygwin uname to windows Git Bash, MSYS2, and Cygwin all report `uname -s` strings like `MINGW64_NT-10.0-26200`, `MSYS_NT-10.0`, or `CYGWIN_NT-10.0`. The POSIX launcher lower-cased that string and used it verbatim as the OS slug, so on Windows it looked for `bin/lumen-mingw64_nt-...-amd64` (skipping the already-installed `bin/lumen-windows-amd64.exe`) and then tried to download `lumen-X.Y.Z-mingw64_nt-...-amd64` from GitHub releases, where no such asset exists. The result was a non-blocking SessionStart hook failure on every Claude Code session start whenever shell:true dispatched through Git Bash instead of cmd.exe (curl exit 22, 404). Map the three Windows POSIX environments to `windows` and append `.exe`, matching scripts/run.cmd and the goreleaser asset names. Add table-driven coverage in scripts/test_run.sh and align the two integration tests' expected binary path with the new normalisation. --- scripts/run | 19 ++++++++++++------ scripts/test_run.sh | 48 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/scripts/run b/scripts/run index b0bda40..b60fb8a 100755 --- a/scripts/run +++ b/scripts/run @@ -6,19 +6,26 @@ set -euo pipefail # OpenCode, and direct local invocation. PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}}" -# Platform detection +# Platform detection. Normalise MinGW/MSYS/Cygwin (Git Bash on Windows) to +# `windows` so the launcher resolves the same binary as scripts/run.cmd and +# the goreleaser asset names on GitHub releases. OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +case "$OS" in + mingw*|msys*|cygwin*) OS="windows" ;; +esac ARCH="$(uname -m)" case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; esac +EXT="" +[ "$OS" = "windows" ] && EXT=".exe" # Find binary: check bin/ first, then goreleaser dist/ output, then download BINARY="" for candidate in \ - "${PLUGIN_ROOT}/bin/lumen" \ - "${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}"; do + "${PLUGIN_ROOT}/bin/lumen${EXT}" \ + "${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}${EXT}"; do if [ -x "$candidate" ]; then BINARY="$candidate" break @@ -27,7 +34,7 @@ done # Download on first run if no binary found if [ -z "$BINARY" ]; then - BINARY="${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}" + BINARY="${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}${EXT}" REPO="ory/lumen" @@ -43,7 +50,7 @@ if [ -z "$BINARY" ]; then exit 1 fi - ASSET="lumen-${VERSION#v}-${OS}-${ARCH}" + ASSET="lumen-${VERSION#v}-${OS}-${ARCH}${EXT}" URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}" echo "Downloading lumen ${VERSION} for ${OS}/${ARCH}..." >&2 @@ -70,7 +77,7 @@ if [ -z "$BINARY" ]; then echo "Falling back to ${LATEST_TAG}..." >&2 VERSION="$LATEST_TAG" - ASSET="lumen-${VERSION#v}-${OS}-${ARCH}" + ASSET="lumen-${VERSION#v}-${OS}-${ARCH}${EXT}" URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}" curl -fL --progress-bar --max-time 300 --retry 3 --retry-delay 2 "$URL" -o "$BINARY" diff --git a/scripts/test_run.sh b/scripts/test_run.sh index f5dadb8..54bb767 100755 --- a/scripts/test_run.sh +++ b/scripts/test_run.sh @@ -59,6 +59,22 @@ normalise_arch() { esac } +# --------------------------------------------------------------------------- +# OS normalisation (mirrors run case statement). Maps Git Bash / MSYS2 / +# Cygwin uname strings to `windows` so the launcher resolves the same +# binary asset as run.cmd. Emits ":" so callers can also verify +# the executable suffix that windows builds require. +# --------------------------------------------------------------------------- +normalise_os() { + local os="$1" + case "$os" in + mingw*|msys*|cygwin*) os="windows" ;; + esac + local ext="" + [ "$os" = "windows" ] && ext=".exe" + echo "${os}:${ext}" +} + echo "=== asset name tests ===" assert_eq "macOS arm64 asset" \ "lumen-0.0.1-alpha.4-darwin-arm64" \ @@ -104,6 +120,28 @@ assert_eq "aarch64 → arm64" "arm64" "$(normalise_arch "aarch64")" assert_eq "arm64 passthrough" "arm64" "$(normalise_arch "arm64")" assert_eq "amd64 passthrough" "amd64" "$(normalise_arch "amd64")" +echo "" +echo "=== OS normalisation tests ===" +# Git Bash / MSYS2 / Cygwin emit uname -s strings that begin with MINGW64_NT, +# MINGW32_NT, MSYS_NT, or CYGWIN_NT. Before this normalisation the launcher +# took those strings verbatim and constructed asset URLs like +# `lumen-X.Y.Z-mingw64_nt-10.0-26200-amd64`, which 404 on GitHub releases +# and skip the already-installed `bin/lumen-windows-amd64.exe`. +assert_eq "MinGW64 Git Bash → windows + .exe" \ + "windows:.exe" "$(normalise_os "mingw64_nt-10.0-26200")" +assert_eq "MinGW32 Git Bash → windows + .exe" \ + "windows:.exe" "$(normalise_os "mingw32_nt-10.0")" +assert_eq "MSYS2 → windows + .exe" \ + "windows:.exe" "$(normalise_os "msys_nt-10.0-26200")" +assert_eq "Cygwin → windows + .exe" \ + "windows:.exe" "$(normalise_os "cygwin_nt-10.0")" +assert_eq "windows passthrough → windows + .exe" \ + "windows:.exe" "$(normalise_os "windows")" +assert_eq "linux passthrough → linux, no ext" \ + "linux:" "$(normalise_os "linux")" +assert_eq "darwin passthrough → darwin, no ext" \ + "darwin:" "$(normalise_os "darwin")" + echo "" echo "=== binary candidate priority tests ===" TMP_DIR="$(mktemp -d)" @@ -168,9 +206,12 @@ echo "=== stdio download-on-first-run integration test ===" trap 'rm -rf "$_TMPROOT" "$_FAKE_CURL_DIR"' EXIT OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$OS" in mingw*|msys*|cygwin*) OS="windows" ;; esac + EXT="" + [ "$OS" = "windows" ] && EXT=".exe" ARCH_RAW="$(uname -m)" case "$ARCH_RAW" in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; *) ARCH="$ARCH_RAW" ;; esac - _EXPECTED_BINARY="${_TMPROOT}/bin/lumen-${OS}-${ARCH}" + _EXPECTED_BINARY="${_TMPROOT}/bin/lumen-${OS}-${ARCH}${EXT}" printf '{\n ".": "0.0.1"\n}\n' > "${_TMPROOT}/.release-please-manifest.json" mkdir -p "${_TMPROOT}/bin" @@ -304,13 +345,16 @@ echo "=== stdio first-install MCP handshake test ===" trap 'rm -rf "$_TMPROOT" "$_FAKE_CURL_DIR" "$_MOCK_BIN_DIR"' EXIT _OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$_OS" in mingw*|msys*|cygwin*) _OS="windows" ;; esac + _EXT="" + [ "$_OS" = "windows" ] && _EXT=".exe" _ARCH_RAW="$(uname -m)" case "$_ARCH_RAW" in x86_64) _ARCH="amd64" ;; aarch64) _ARCH="arm64" ;; *) _ARCH="$_ARCH_RAW" ;; esac - _EXPECTED_BINARY="${_TMPROOT}/bin/lumen-${_OS}-${_ARCH}" + _EXPECTED_BINARY="${_TMPROOT}/bin/lumen-${_OS}-${_ARCH}${_EXT}" _MOCK_BIN="${_MOCK_BIN_DIR}/mock_lumen" if ! (cd "${_REPO_ROOT}" && CGO_ENABLED=0 go build -o "${_MOCK_BIN}" ./scripts/testdata/mock_mcp_server) >"${_TMPROOT}/mock_build.log" 2>&1; then From 7c727b764e0226346a0c3cdaedd15e083c57b194 Mon Sep 17 00:00:00 2001 From: hypn4 Date: Thu, 14 May 2026 00:13:06 +0900 Subject: [PATCH 2/2] fix(scripts): keep extensionless dev-build candidate on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local development uses `make build-local`, which runs `go build -o bin/lumen .`. Go does NOT append `.exe` when `-o` is explicit, so on Windows the dev build is written to `bin/lumen` (no extension). The previous commit only looked for `bin/lumen.exe` and `bin/lumen-windows-amd64.exe`, which would silently fall through to a GitHub download path during `claude --plugin-dir .` development — regressing the dev workflow that the original two-candidate list was designed to preserve. Add `bin/lumen` (without EXT) as the middle candidate so the priority on Windows becomes: .exe dev build → extensionless dev build → downloaded artefact. Linux/macOS retain their existing two-candidate behavior (the duplicate is a no-op there). Add three table-driven tests covering the candidate ordering across windows (.exe), mingw-normalised, and linux configurations. --- scripts/run | 6 +++++- scripts/test_run.sh | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/run b/scripts/run index b60fb8a..457cb2e 100755 --- a/scripts/run +++ b/scripts/run @@ -21,10 +21,14 @@ esac EXT="" [ "$OS" = "windows" ] && EXT=".exe" -# Find binary: check bin/ first, then goreleaser dist/ output, then download +# Find binary: check bin/ first, then goreleaser dist/ output, then download. +# `make build-local` runs `go build -o bin/lumen .` which, even on Windows, +# does NOT append .exe when -o is explicit — so we must keep the +# extensionless dev-build candidate alongside the .exe one. BINARY="" for candidate in \ "${PLUGIN_ROOT}/bin/lumen${EXT}" \ + "${PLUGIN_ROOT}/bin/lumen" \ "${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}${EXT}"; do if [ -x "$candidate" ]; then BINARY="$candidate" diff --git a/scripts/test_run.sh b/scripts/test_run.sh index 54bb767..c05dfc6 100755 --- a/scripts/test_run.sh +++ b/scripts/test_run.sh @@ -144,6 +144,30 @@ assert_eq "darwin passthrough → darwin, no ext" \ echo "" echo "=== binary candidate priority tests ===" +# Windows dev-build case: `make build-local` runs `go build -o bin/lumen .` +# which produces an extensionless `bin/lumen` even on Windows. The launcher +# must still discover it ahead of falling back to the downloaded artefact. +# We test the candidate ordering directly (no [ -x ] dependency, since Git +# Bash refuses to mark extensionless touch-created files as executable — +# the pre-existing two `[ -x ]`-based tests below fail on Git Bash for that +# reason, unrelated to this fix). +expected_candidates() { + local os="$1" arch="$2" + local ext="" + case "$os" in mingw*|msys*|cygwin*) os="windows" ;; esac + [ "$os" = "windows" ] && ext=".exe" + printf 'bin/lumen%s\nbin/lumen\nbin/lumen-%s-%s%s\n' "$ext" "$os" "$arch" "$ext" +} +assert_eq "windows candidates: .exe, extensionless dev, downloaded" \ + "$(printf 'bin/lumen.exe\nbin/lumen\nbin/lumen-windows-amd64.exe\n')" \ + "$(expected_candidates windows amd64)" +assert_eq "mingw normalises, keeps extensionless dev build candidate" \ + "$(printf 'bin/lumen.exe\nbin/lumen\nbin/lumen-windows-amd64.exe\n')" \ + "$(expected_candidates mingw64_nt-10.0-26200 amd64)" +assert_eq "linux candidates: extensionless dev, downloaded" \ + "$(printf 'bin/lumen\nbin/lumen\nbin/lumen-linux-amd64\n')" \ + "$(expected_candidates linux amd64)" + TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT