diff --git a/scripts/run b/scripts/run index b0bda40..457cb2e 100755 --- a/scripts/run +++ b/scripts/run @@ -6,19 +6,30 @@ 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 +# 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}"; do + "${PLUGIN_ROOT}/bin/lumen-${OS}-${ARCH}${EXT}"; do if [ -x "$candidate" ]; then BINARY="$candidate" break @@ -27,7 +38,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 +54,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 +81,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..c05dfc6 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,8 +120,54 @@ 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 ===" +# 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 @@ -168,9 +230,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 +369,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