#!/usr/bin/env bash # Verify the local code-intelligence toolchain (ersatztv#777). # # Operator-run, NOT a CI gate: every dependency it checks is a developer-machine # install (Homebrew dotnet, a global csharp-ls, web/node_modules), so there is no # runner on which a red here would mean anything. It exists because the #777 root # causes were both environment divergence — a config that silently pointed at a # path this machine does not have — and that class is invisible until something # looks. See docs/local-lsp-tooling.md. # # Exit 0 = every check ran AND passed. Exit 1 = at least one FAIL *or* SKIP — # a skipped check is a non-result, not a success. set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" failures=0 checks=0 skipped=0 pass() { checks=$((checks + 1)); printf ' PASS %s\n' "$1"; } fail() { checks=$((checks + 1)); failures=$((failures + 1)); printf ' FAIL %s\n' "$1"; } # A skip is NOT a pass. Counting it as one would let the summary read "all checks # passed" while the most important check never ran. skip() { checks=$((checks + 1)); skipped=$((skipped + 1)); printf ' SKIP %s\n' "$1"; } note() { printf ' %s\n' "$1"; } echo "Local LSP toolchain check — $REPO_ROOT" echo # --------------------------------------------------------------------------- echo "csharp-ls (the LSP tool's C# server)" if ! command -v csharp-ls >/dev/null 2>&1; then fail "csharp-ls is not on PATH" note "install: dotnet tool install --global csharp-ls" else pass "csharp-ls on PATH ($(command -v csharp-ls))" fi # MSBuildLocator resolves the SDK next to the dotnet host it finds, and needs a # sibling host/fxr. Homebrew's bin/dotnet has none — its real root is libexec. dotnet_root="${DOTNET_ROOT:-}" if [ -z "$dotnet_root" ]; then if [ -n "${DOTNET_HOST_PATH:-}" ]; then dotnet_root="$(dirname "$DOTNET_HOST_PATH")" elif command -v dotnet >/dev/null 2>&1; then dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)" 2>/dev/null || command -v dotnet)")" fi fi if [ -z "$dotnet_root" ]; then fail "no dotnet host could be resolved (DOTNET_ROOT, DOTNET_HOST_PATH, PATH all empty)" elif compgen -G "$dotnet_root/host/fxr/*/libhostfxr.*" >/dev/null 2>&1; then pass "dotnet root has host/fxr ($dotnet_root)" else fail "dotnet root has NO host/fxr — csharp-ls cannot initialize ($dotnet_root)" note "this is the #777 failure: set DOTNET_ROOT to the root that owns host/fxr," note "e.g. /opt/homebrew/opt/dotnet/libexec on a Homebrew install." note "Claude Code picks it up from .claude/settings.local.json -> env.DOTNET_ROOT" note "(local, not the tracked settings.json — the value is machine-specific)." fi # --------------------------------------------------------------------------- echo echo "typescript-language-server (the LSP tool's TS server)" if ! command -v typescript-language-server >/dev/null 2>&1; then fail "typescript-language-server is not on PATH" note "install: npm install -g typescript-language-server" else pass "typescript-language-server on PATH" fi # v5 has no --tsserver-path and the plugin cannot pass initializationOptions, so # the ONLY lever is making `typescript` resolvable from the workspace root, which # for Claude Code is the repo root — not web/. if [ -e "$REPO_ROOT/node_modules/typescript/lib/tsserver.js" ]; then pass "typescript resolvable from the repo root" else fail "typescript NOT resolvable from the repo root — the TS server will refuse to start" note "the package lives in web/node_modules; link it at the root:" note " mkdir -p '$REPO_ROOT/node_modules'" note " ln -sfn '$REPO_ROOT/web/node_modules/typescript' '$REPO_ROOT/node_modules/typescript'" fi # --------------------------------------------------------------------------- echo echo "csharp-lsp MCP server (the C# path subagents can actually reach)" mcp_json="$REPO_ROOT/.mcp.json" if [ ! -f "$mcp_json" ]; then fail ".mcp.json not present (it is gitignored — see docs/local-lsp-tooling.md to recreate it)" elif [ "${SKIP_MCP_SMOKE:-0}" = "1" ]; then skip "csharp-lsp MCP server (SKIP_MCP_SMOKE=1) — NOT verified" elif ! command -v perl >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then fail "cannot run the MCP smoke test: perl and python3 are both required" else # Actually START the server and list its tools. Checking `[ -x command ]` instead # would be vacuous: a DIRECTORY satisfies -x (`[ -x /bin ]` is true), so that # predicate can report a pass for a server that cannot run at all. # # `exec @ARGV or die` is load-bearing. Without the `or die`, perl exits 0 when it # cannot exec the command, the `if` reads that as success, and this branch prints # a PASS having run nothing — the same false green one level up. note "starting the MCP server (this takes ~30-60s on a cold build)..." if smoke_out="$(perl -e 'alarm shift; exec @ARGV or die "exec failed: $!\n"' 240 \ python3 "$REPO_ROOT/scripts/mcp_smoke.py" "$mcp_json" csharp-lsp 200 \ --expect-server csharp-lsp-mcp \ --expect-tool csharp_set_workspace --expect-tool csharp_references 2>&1)"; then pass "csharp-lsp MCP server starts and serves tools — ${smoke_out#OK: }" else fail "csharp-lsp MCP server did not come up" note "${smoke_out:-(no output — the smoke test itself was killed)}" note "#777's failure was an entry naming a dotnet install that no longer existed;" note "a net8.0-targeted vendored clone on an SDK-10-only host fails the same way." fi fi # --------------------------------------------------------------------------- echo passed=$(( checks - failures - skipped )) if [ "$failures" -eq 0 ] && [ "$skipped" -eq 0 ]; then echo "All $checks checks passed." elif [ "$failures" -eq 0 ]; then echo "$passed passed, $skipped SKIPPED of $checks — a skipped check is not a passed one." else echo "$failures of $checks checks FAILED ($skipped skipped) — see docs/local-lsp-tooling.md." fi # A skip is a non-result, so it is not success: exit non-zero unless everything ran. exit $(( (failures > 0 || skipped > 0) ? 1 : 0 ))