From b228378597025172e88e3bf915576f7bbf7408a4 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:54:33 -0700 Subject: [PATCH 01/11] chore: initialize execution context for installation-distribution From f015762e9fd4d8c215b3b397b7ec0796cee92d4b Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:55:04 -0700 Subject: [PATCH 02/11] feat: write install_method marker in install.sh (git) --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index 30c3498..764703f 100755 --- a/install.sh +++ b/install.sh @@ -1199,6 +1199,8 @@ if [[ $INSTALL_GLOBAL =~ ^[Yy] ]]; then CONFIG_DIR="$HOME/.config/codetect" mkdir -p "$CONFIG_DIR" CONFIG_FILE="$CONFIG_DIR/config.env" + # Record install method for codetect update + echo "git" > "$CONFIG_DIR/install_method" INSTALLED_GLOBALLY=true else warn "Skipping global installation" From 4532b0c53e7ffa0fba8df239f460b688c238ce71 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:55:35 -0700 Subject: [PATCH 03/11] feat: add check_for_updates() with 24h throttle to wrapper --- scripts/codetect-wrapper.sh | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/scripts/codetect-wrapper.sh b/scripts/codetect-wrapper.sh index f64cb2b..91419d2 100755 --- a/scripts/codetect-wrapper.sh +++ b/scripts/codetect-wrapper.sh @@ -1321,6 +1321,32 @@ cmd_help() { echo " claude # Start Claude Code" } +# +# Version staleness check (throttled to once per 24 hours) +# +check_for_updates() { + local stamp_file="$CONFIG_DIR/last_update_check" + local now + now=$(date +%s 2>/dev/null) || return 0 + local last + last=$(cat "$stamp_file" 2>/dev/null) || last=0 + if (( now - last < 86400 )); then + return 0 + fi + echo "$now" > "$stamp_file" 2>/dev/null || true + local latest + latest=$(curl -sf --max-time 3 \ + "https://api.github.com/repos/brian-lai/codetect/releases/latest" \ + 2>/dev/null | grep '"tag_name"' | cut -d'"' -f4) || return 0 + [[ -z "$latest" ]] && return 0 + # Get current version tag (strip leading "codetect " prefix if present) + local current + current=$(cmd_version 2>/dev/null | awk '{print $NF}') || return 0 + if [[ -n "$current" && "$latest" != "$current" ]]; then + echo -e " ${CYAN}ℹ${NC} codetect $latest is available. Run: codetect update" >&2 + fi +} + # # Main # @@ -1328,6 +1354,11 @@ main() { local cmd="${1:-help}" shift || true + # Check for updates on every command except mcp (long-running server) + if [[ "$cmd" != "mcp" ]]; then + check_for_updates + fi + case "$cmd" in mcp) cmd_mcp "$@" From 2dce803f48965f7e048bf814eae99df20308db94 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:56:47 -0700 Subject: [PATCH 04/11] feat: install-method-aware cmd_update(), version fallback, update.sh messaging --- Makefile | 4 +++ scripts/codetect-wrapper.sh | 61 ++++++++++++++++++++++++++++++++----- scripts/update.sh | 8 +++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 920067a..f6335eb 100644 --- a/Makefile +++ b/Makefile @@ -164,6 +164,10 @@ install: build @chmod +x $(BIN_DIR)/codetect $(BIN_DIR)/codetect-mcp $(BIN_DIR)/codetect-index $(BIN_DIR)/codetect-daemon $(BIN_DIR)/codetect-eval $(BIN_DIR)/migrate-to-postgres @codesign --sign - --force $(BIN_DIR)/codetect-mcp $(BIN_DIR)/codetect-index $(BIN_DIR)/codetect-daemon $(BIN_DIR)/codetect-eval $(BIN_DIR)/migrate-to-postgres 2>/dev/null || true @cp templates/mcp.json $(SHARE_DIR)/templates/ + @# Write VERSION file so non-git installs can report their version + @git describe --tags --exact-match 2>/dev/null > $(SHARE_DIR)/VERSION || \ + git describe --tags 2>/dev/null > $(SHARE_DIR)/VERSION || \ + echo "dev" > $(SHARE_DIR)/VERSION @echo "" @echo "✓ Installed to $(PREFIX)" @echo "" diff --git a/scripts/codetect-wrapper.sh b/scripts/codetect-wrapper.sh index 91419d2..1dd4e7d 100755 --- a/scripts/codetect-wrapper.sh +++ b/scripts/codetect-wrapper.sh @@ -1244,6 +1244,12 @@ cmd_version() { fi fi + # For brew/binary installs: read VERSION file written at install time + if [[ -f "$SHARE_DIR/VERSION" ]]; then + echo "codetect $(cat "$SHARE_DIR/VERSION")" + return 0 + fi + # Fallback: ask codetect-index for its version if [[ -x "$BIN_DIR/codetect-index" ]]; then "$BIN_DIR/codetect-index" version @@ -1254,16 +1260,55 @@ cmd_version() { } cmd_update() { - local source_dir="${CODETECT_SOURCE:-$HOME/dev/codetect}" - - if [[ ! -f "$source_dir/scripts/update.sh" ]]; then - error "Update script not found" - info "Set CODETECT_SOURCE to the location of your codetect clone" - info "Default: $source_dir" - return 1 + # Detect how codetect was installed + local method + method=$(cat "$CONFIG_DIR/install_method" 2>/dev/null) || method="" + + # Detect go install if marker absent: binary lives under GOPATH/bin + if [[ -z "$method" ]]; then + local codetect_bin + codetect_bin=$(which codetect 2>/dev/null) || codetect_bin="" + local gopath + gopath=$(go env GOPATH 2>/dev/null) || gopath="" + if [[ -n "$gopath" && "$codetect_bin" == "$gopath/bin/"* ]]; then + method="go" + fi fi - exec "$source_dir/scripts/update.sh" "$@" + case "$method" in + brew) + info "Installed via Homebrew. Running: brew upgrade codetect" + exec brew upgrade codetect + ;; + go) + info "Installed via go install." + info "Updating codetect-mcp: go install github.com/brian-lai/codetect/cmd/codetect@latest" + exec go install github.com/brian-lai/codetect/cmd/codetect@latest + ;; + binary) + local installer="$SHARE_DIR/install-binary.sh" + if [[ -x "$installer" ]]; then + exec "$installer" "$@" + else + info "Re-run the installer to update:" + info " curl -fsSL https://raw.githubusercontent.com/brian-lai/codetect/main/scripts/install-binary.sh | bash" + return 0 + fi + ;; + git|*) + # Legacy / git-clone path — delegate to scripts/update.sh + local source_dir="${CODETECT_SOURCE:-$HOME/dev/codetect}" + if [[ ! -f "$source_dir/scripts/update.sh" ]]; then + error "Update script not found and install method unknown." + info "Please reinstall codetect using one of:" + info " brew install brian-lai/tap/codetect" + info " curl -fsSL https://raw.githubusercontent.com/brian-lai/codetect/main/scripts/install-binary.sh | bash" + info " https://github.com/brian-lai/codetect#installation" + return 1 + fi + exec "$source_dir/scripts/update.sh" "$@" + ;; + esac } cmd_help() { diff --git a/scripts/update.sh b/scripts/update.sh index b62d940..941f02f 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -74,9 +74,13 @@ echo "" # Check if source directory exists if [[ ! -d "$SOURCE_DIR" ]]; then error "Source directory not found: $SOURCE_DIR" - info "Set CODETECT_SOURCE to the location of your codetect clone" - info "Or clone it:" + echo "" + info "This updater requires a git clone of codetect." + info "If you installed via Homebrew, curl, or apt, run 'codetect update' instead." + echo "" + info "To use this script, clone the repo first:" info " git clone https://github.com/brian-lai/codetect.git $SOURCE_DIR" + info "Or set CODETECT_SOURCE to your existing clone location." exit 1 fi From d3395580151eaa0ae015025cfd6fe79f5020e9d3 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:57:07 -0700 Subject: [PATCH 05/11] feat: add Homebrew formula Formula/codetect.rb --- Formula/codetect.rb | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Formula/codetect.rb diff --git a/Formula/codetect.rb b/Formula/codetect.rb new file mode 100644 index 0000000..dc2ece5 --- /dev/null +++ b/Formula/codetect.rb @@ -0,0 +1,47 @@ +# Homebrew formula for codetect +# +# This file is the reference formula. The canonical, published version lives in +# the homebrew tap repo: https://github.com/brian-lai/homebrew-tap +# +# To install: +# brew tap brian-lai/tap +# brew install codetect +# +# To update this formula after a new release, update `url`, `sha256`, and `version`. +# The release workflow (`.github/workflows/release.yml`) should automate this. + +class Codetect < Formula + desc "Fast, token-efficient codebase search MCP server for Claude Code and any LLM" + homepage "https://github.com/brian-lai/codetect" + url "https://github.com/brian-lai/codetect/archive/refs/tags/v3.7.5.tar.gz" + sha256 "PLACEHOLDER_SHA256_UPDATE_ON_RELEASE" + license "MIT" + version "3.7.5" + + depends_on "go" => :build + depends_on "ripgrep" + + def install + system "make", "build" + bin.install "dist/codetect" => "codetect-mcp" + bin.install "dist/codetect-index" + bin.install "dist/codetect-daemon" + bin.install "dist/codetect-eval" + bin.install "scripts/codetect-wrapper.sh" => "codetect" + (share/"codetect/templates").install Dir["templates/*"] + # Write VERSION file for non-git version reporting + (share/"codetect/VERSION").write(version.to_s + "\n") + # Copy the binary installer for `codetect update` + (share/"codetect").install "scripts/install-binary.sh" + end + + def post_install + config_dir = Pathname.new(ENV["HOME"]) / ".config/codetect" + config_dir.mkpath + (config_dir / "install_method").write("brew\n") + end + + test do + assert_match version.to_s, shell_output("#{bin}/codetect version") + end +end From ef7255906bd35fc24681540fb2181c599085f917 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:57:42 -0700 Subject: [PATCH 06/11] feat: add scripts/install-binary.sh for curl one-liner install --- scripts/install-binary.sh | 209 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100755 scripts/install-binary.sh diff --git a/scripts/install-binary.sh b/scripts/install-binary.sh new file mode 100755 index 0000000..4ce5396 --- /dev/null +++ b/scripts/install-binary.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# +# codetect binary installer +# +# Downloads the latest pre-built codetect release from GitHub and installs it +# to ~/.local/bin. No Go toolchain required. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/brian-lai/codetect/main/scripts/install-binary.sh | bash +# bash install-binary.sh [--version v3.7.5] [--prefix /usr/local] +# + +set -e + +REPO="brian-lai/codetect" +INSTALL_PREFIX="${CODETECT_PREFIX:-$HOME/.local}" +BIN_DIR="$INSTALL_PREFIX/bin" +SHARE_DIR="$INSTALL_PREFIX/share/codetect" +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/codetect" +SPECIFIC_VERSION="" + +# Parse flags +while [[ $# -gt 0 ]]; do + case "$1" in + --version|-v) + SPECIFIC_VERSION="$2" + shift 2 + ;; + --prefix) + INSTALL_PREFIX="$2" + BIN_DIR="$INSTALL_PREFIX/bin" + SHARE_DIR="$INSTALL_PREFIX/share/codetect" + shift 2 + ;; + *) + shift + ;; + esac +done + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +success() { echo -e "${GREEN}✓${NC} $1"; } +warn() { echo -e "${YELLOW}!${NC} $1"; } +error() { echo -e "${RED}✗${NC} $1"; } +info() { echo -e " $1"; } + +echo -e "${CYAN}Installing codetect...${NC}" +echo "" + +# Detect OS and architecture +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) + error "Unsupported architecture: $ARCH" + info "Please build from source: https://github.com/$REPO#installation" + exit 1 + ;; +esac + +if [[ "$OS" != "linux" && "$OS" != "darwin" ]]; then + error "Unsupported OS: $OS" + info "Please build from source: https://github.com/$REPO#installation" + exit 1 +fi + +info "Platform: $OS/$ARCH" +echo "" + +# Resolve version to install +if [[ -z "$SPECIFIC_VERSION" ]]; then + info "Fetching latest release..." + SPECIFIC_VERSION=$(curl -sf --max-time 10 \ + "https://api.github.com/repos/$REPO/releases/latest" \ + | grep '"tag_name"' | cut -d'"' -f4) + if [[ -z "$SPECIFIC_VERSION" ]]; then + error "Could not determine latest version. Check your network connection." + exit 1 + fi +fi + +# Strip leading 'v' for filename construction, keep full tag for URL +VERSION_TAG="$SPECIFIC_VERSION" +VERSION_NUM="${VERSION_TAG#v}" + +TARBALL="codetect_${OS}_${ARCH}.tar.gz" +DOWNLOAD_URL="https://github.com/$REPO/releases/download/${VERSION_TAG}/${TARBALL}" +CHECKSUMS_URL="https://github.com/$REPO/releases/download/${VERSION_TAG}/checksums.txt" + +info "Version: $VERSION_TAG" +info "Downloading: $TARBALL" +echo "" + +# Create temp directory +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +# Download tarball and checksums +curl -fL --max-time 120 --progress-bar -o "$TMP_DIR/$TARBALL" "$DOWNLOAD_URL" || { + error "Download failed: $DOWNLOAD_URL" + info "Make sure release $VERSION_TAG has pre-built binaries attached." + info "Check: https://github.com/$REPO/releases/$VERSION_TAG" + exit 1 +} + +# Verify checksum if available +if curl -sf --max-time 10 -o "$TMP_DIR/checksums.txt" "$CHECKSUMS_URL" 2>/dev/null; then + info "Verifying checksum..." + expected=$(grep "$TARBALL" "$TMP_DIR/checksums.txt" | awk '{print $1}') + if [[ -n "$expected" ]]; then + if command -v sha256sum &>/dev/null; then + actual=$(sha256sum "$TMP_DIR/$TARBALL" | awk '{print $1}') + elif command -v shasum &>/dev/null; then + actual=$(shasum -a 256 "$TMP_DIR/$TARBALL" | awk '{print $1}') + else + warn "sha256sum/shasum not found; skipping checksum verification" + actual="$expected" + fi + if [[ "$actual" != "$expected" ]]; then + error "Checksum mismatch!" + info "Expected: $expected" + info "Got: $actual" + exit 1 + fi + success "Checksum verified" + fi +fi + +# Extract +info "Extracting..." +tar -xzf "$TMP_DIR/$TARBALL" -C "$TMP_DIR" + +# Install +info "Installing to $BIN_DIR..." +mkdir -p "$BIN_DIR" "$SHARE_DIR/templates" "$CONFIG_DIR" + +for bin in codetect-mcp codetect-index codetect-daemon codetect-eval migrate-to-postgres; do + if [[ -f "$TMP_DIR/$bin" ]]; then + cp "$TMP_DIR/$bin" "$BIN_DIR/$bin" + chmod +x "$BIN_DIR/$bin" + fi +done + +# The wrapper script is the main user-facing entry point +if [[ -f "$TMP_DIR/codetect" ]]; then + cp "$TMP_DIR/codetect" "$BIN_DIR/codetect" + chmod +x "$BIN_DIR/codetect" +fi + +# Templates +if [[ -d "$TMP_DIR/templates" ]]; then + cp -r "$TMP_DIR/templates/." "$SHARE_DIR/templates/" +fi + +# Store VERSION and a copy of this installer for `codetect update` +echo "$VERSION_NUM" > "$SHARE_DIR/VERSION" +cp "$0" "$SHARE_DIR/install-binary.sh" 2>/dev/null || \ + curl -fsSL --max-time 10 \ + "https://raw.githubusercontent.com/$REPO/main/scripts/install-binary.sh" \ + -o "$SHARE_DIR/install-binary.sh" 2>/dev/null || true +chmod +x "$SHARE_DIR/install-binary.sh" 2>/dev/null || true + +# Record install method +echo "binary" > "$CONFIG_DIR/install_method" + +success "Installed codetect $VERSION_NUM" +echo "" + +# PATH check +if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then + warn "$BIN_DIR is not in your PATH" + echo "" + info "Add this to your shell profile (~/.zshrc or ~/.bashrc):" + echo "" + echo -e " ${YELLOW}export PATH=\"$BIN_DIR:\$PATH\"${NC}" + echo "" + if [[ $SHELL == *"zsh"* ]]; then + SHELL_RC="$HOME/.zshrc" + else + SHELL_RC="$HOME/.bashrc" + fi + read -r -p " Add to $SHELL_RC now? [Y/n] " ADD_PATH + ADD_PATH=${ADD_PATH:-Y} + if [[ $ADD_PATH =~ ^[Yy] ]]; then + echo "" >> "$SHELL_RC" + echo "# Added by codetect installer" >> "$SHELL_RC" + echo "export PATH=\"$BIN_DIR:\$PATH\"" >> "$SHELL_RC" + success "Added to $SHELL_RC" + info "Run: source $SHELL_RC" + fi +fi + +echo "" +echo -e "${GREEN}Done!${NC} Get started:" +echo "" +echo " cd /path/to/your/project" +echo " codetect init # Create .mcp.json" +echo " codetect index # Index symbols + embeddings" +echo " claude # Start Claude Code" +echo "" +echo "See https://github.com/$REPO for full documentation." From a90bbf10ab45090ad3a158e702bcd3bb6c53dc50 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:58:18 -0700 Subject: [PATCH 07/11] feat: add packaging/deb/ control file, postinst, and build script --- packaging/deb/DEBIAN/control | 14 +++++++ packaging/deb/DEBIAN/postinst | 10 +++++ packaging/deb/build-deb.sh | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 packaging/deb/DEBIAN/control create mode 100755 packaging/deb/DEBIAN/postinst create mode 100755 packaging/deb/build-deb.sh diff --git a/packaging/deb/DEBIAN/control b/packaging/deb/DEBIAN/control new file mode 100644 index 0000000..1fe985b --- /dev/null +++ b/packaging/deb/DEBIAN/control @@ -0,0 +1,14 @@ +Package: codetect +Version: VERSION_PLACEHOLDER +Architecture: ARCH_PLACEHOLDER +Maintainer: Brian Lai +Depends: ripgrep +Recommends: ollama +Section: devel +Priority: optional +Homepage: https://github.com/brian-lai/codetect +Description: Fast, token-efficient codebase search MCP server + codetect is a local MCP server that brings Cursor-like codebase intelligence + to Claude Code and any LLM tool. It provides fast keyword search (ripgrep), + symbol indexing (AST-based), and semantic search (Ollama embeddings) through + four focused MCP tools. diff --git a/packaging/deb/DEBIAN/postinst b/packaging/deb/DEBIAN/postinst new file mode 100755 index 0000000..d0f444d --- /dev/null +++ b/packaging/deb/DEBIAN/postinst @@ -0,0 +1,10 @@ +#!/bin/bash +# Post-installation script for codetect .deb package +set -e + +# Record install method for codetect update +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/codetect" +mkdir -p "$CONFIG_DIR" +echo "binary" > "$CONFIG_DIR/install_method" + +exit 0 diff --git a/packaging/deb/build-deb.sh b/packaging/deb/build-deb.sh new file mode 100755 index 0000000..12f4159 --- /dev/null +++ b/packaging/deb/build-deb.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Build a .deb package for codetect. +# +# Usage (from repo root): +# bash packaging/deb/build-deb.sh [version] [arch] +# +# Examples: +# bash packaging/deb/build-deb.sh v3.7.5 amd64 +# bash packaging/deb/build-deb.sh v3.7.5 arm64 +# +# Outputs: dist/codetect__.deb +# +# Prerequisites: dpkg-deb, built binaries in dist/ + +set -e + +VERSION="${1:-$(git describe --tags --exact-match 2>/dev/null || echo dev)}" +VERSION_NUM="${VERSION#v}" +ARCH="${2:-amd64}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +PKG_DIR="$REPO_ROOT/dist/deb/codetect_${VERSION_NUM}_${ARCH}" +DEB_OUT="$REPO_ROOT/dist/codetect_${VERSION_NUM}_${ARCH}.deb" + +echo "Building codetect $VERSION_NUM ($ARCH) .deb..." + +# Clean and create package tree +rm -rf "$PKG_DIR" +mkdir -p \ + "$PKG_DIR/DEBIAN" \ + "$PKG_DIR/usr/local/bin" \ + "$PKG_DIR/usr/local/share/codetect/templates" + +# Fill DEBIAN control files +sed -e "s/VERSION_PLACEHOLDER/$VERSION_NUM/" \ + -e "s/ARCH_PLACEHOLDER/$ARCH/" \ + "$SCRIPT_DIR/DEBIAN/control" > "$PKG_DIR/DEBIAN/control" + +install -m 755 "$SCRIPT_DIR/DEBIAN/postinst" "$PKG_DIR/DEBIAN/postinst" + +# Install binaries (must be pre-built) +for bin in codetect-mcp codetect-index codetect-daemon codetect-eval migrate-to-postgres; do + src="$REPO_ROOT/dist/$bin" + if [[ ! -f "$src" ]]; then + echo "Error: $src not found. Run 'make build' first." >&2 + exit 1 + fi + install -m 755 "$src" "$PKG_DIR/usr/local/bin/$bin" +done + +# Wrapper script (main user-facing entry point) +install -m 755 "$REPO_ROOT/scripts/codetect-wrapper.sh" "$PKG_DIR/usr/local/bin/codetect" + +# Templates +cp -r "$REPO_ROOT/templates/." "$PKG_DIR/usr/local/share/codetect/templates/" + +# VERSION file +echo "$VERSION_NUM" > "$PKG_DIR/usr/local/share/codetect/VERSION" + +# Copy binary installer for `codetect update` +install -m 755 "$REPO_ROOT/scripts/install-binary.sh" \ + "$PKG_DIR/usr/local/share/codetect/install-binary.sh" + +# Build the .deb +dpkg-deb --build "$PKG_DIR" "$DEB_OUT" + +echo "" +echo "Built: $DEB_OUT" +echo "" +echo "Install with:" +echo " sudo dpkg -i $DEB_OUT" From e7ee734b8b291e67892d32c858ff499ec28146cb Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 19:59:01 -0700 Subject: [PATCH 08/11] docs: update README Quick Start with all install methods --- README.md | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c4056e5..970d207 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,27 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and [Migration Guide](docs/ ## Quick Start +**macOS (Homebrew):** ```bash -# Clone and run interactive installer -git clone https://github.com/brian-lai/codetect.git -cd codetect -./install.sh +brew tap brian-lai/tap +brew install codetect ``` -The installer will: -- ✓ Check for required dependencies (Go, ripgrep) -- ✓ Guide you through Ollama setup for semantic search (with prominent warnings if missing) -- ✓ Build and install globally to `~/.local/bin` -- ✓ Configure your shell PATH automatically +**Linux / macOS (curl):** +```bash +curl -fsSL https://raw.githubusercontent.com/brian-lai/codetect/main/scripts/install-binary.sh | bash +``` + +**Debian / Ubuntu (.deb):** +```bash +curl -LO https://github.com/brian-lai/codetect/releases/latest/download/codetect_amd64.deb +sudo dpkg -i codetect_amd64.deb +``` + +**From source:** +```bash +git clone https://github.com/brian-lai/codetect.git && cd codetect && ./install.sh +``` Then in any project: @@ -59,9 +68,9 @@ See [Installation Guide](docs/installation.md) for detailed setup instructions. | Dependency | Required | Purpose | |------------|----------|---------| -| Go 1.21+ | Yes | Building from source | -| [ripgrep](https://github.com/BurntSushi/ripgrep) | Yes | Keyword search | +| [ripgrep](https://github.com/BurntSushi/ripgrep) | Yes | Keyword search (auto-installed by Homebrew) | | [Ollama](https://ollama.ai) | No | Semantic search (local embeddings) | +| Go 1.21+ | Build from source only | Compiling binaries | **Note:** v3 uses ast-grep for symbol extraction. No external ctags dependency required. From 1153ddb0047e722ee75d3d6e772f26132c3805de Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 20:10:03 -0700 Subject: [PATCH 09/11] fix: address all code review issues - check_for_updates: write timestamp only after successful API response - check_for_updates: suppress nag for dev builds (version looks like '(abc1234)') - install-binary.sh: gate read prompt behind [[ -t 0 ]] to fix curl|bash pipe - install-binary.sh: prefer re-download over cp "$0" for pipe installs - packaging/deb/postinst: resolve actual user home via SUDO_USER for dpkg root runs - Makefile: use git describe --abbrev=0 to avoid dirty suffix in VERSION file - Formula/codetect.rb: wrap post_install in rescue so write failure is non-fatal - cmd_update go branch: clarify only codetect-mcp is updated via go install --- Formula/codetect.rb | 12 +++++++++--- Makefile | 5 +++-- packaging/deb/DEBIAN/postinst | 11 ++++++++++- scripts/codetect-wrapper.sh | 13 +++++++++---- scripts/install-binary.sh | 14 +++++++++++--- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Formula/codetect.rb b/Formula/codetect.rb index dc2ece5..2486ac2 100644 --- a/Formula/codetect.rb +++ b/Formula/codetect.rb @@ -36,9 +36,15 @@ def install end def post_install - config_dir = Pathname.new(ENV["HOME"]) / ".config/codetect" - config_dir.mkpath - (config_dir / "install_method").write("brew\n") + # ENV["HOME"] may not be the installing user's home in all sandbox contexts, + # but writing the marker is best-effort — never fail the install over it. + begin + config_dir = Pathname.new(ENV.fetch("HOME", Dir.home)) / ".config/codetect" + config_dir.mkpath + (config_dir / "install_method").write("brew\n") + rescue StandardError + # Non-fatal: codetect update will still work via brew upgrade + end end test do diff --git a/Makefile b/Makefile index f6335eb..65357a8 100644 --- a/Makefile +++ b/Makefile @@ -164,9 +164,10 @@ install: build @chmod +x $(BIN_DIR)/codetect $(BIN_DIR)/codetect-mcp $(BIN_DIR)/codetect-index $(BIN_DIR)/codetect-daemon $(BIN_DIR)/codetect-eval $(BIN_DIR)/migrate-to-postgres @codesign --sign - --force $(BIN_DIR)/codetect-mcp $(BIN_DIR)/codetect-index $(BIN_DIR)/codetect-daemon $(BIN_DIR)/codetect-eval $(BIN_DIR)/migrate-to-postgres 2>/dev/null || true @cp templates/mcp.json $(SHARE_DIR)/templates/ - @# Write VERSION file so non-git installs can report their version + @# Write VERSION file so non-git installs can report their version. + @# Use --abbrev=0 to get the nearest tag without the dirty commit suffix. @git describe --tags --exact-match 2>/dev/null > $(SHARE_DIR)/VERSION || \ - git describe --tags 2>/dev/null > $(SHARE_DIR)/VERSION || \ + git describe --tags --abbrev=0 2>/dev/null > $(SHARE_DIR)/VERSION || \ echo "dev" > $(SHARE_DIR)/VERSION @echo "" @echo "✓ Installed to $(PREFIX)" diff --git a/packaging/deb/DEBIAN/postinst b/packaging/deb/DEBIAN/postinst index d0f444d..635165b 100755 --- a/packaging/deb/DEBIAN/postinst +++ b/packaging/deb/DEBIAN/postinst @@ -2,8 +2,17 @@ # Post-installation script for codetect .deb package set -e +# Resolve the actual installing user's home directory. +# When run via "sudo dpkg -i", $HOME is /root but SUDO_USER is the real user. +if [[ -n "$SUDO_USER" ]]; then + USER_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) +else + USER_HOME="$HOME" +fi + # Record install method for codetect update -CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/codetect" +XDG_CONFIG="${XDG_CONFIG_HOME:-$USER_HOME/.config}" +CONFIG_DIR="$XDG_CONFIG/codetect" mkdir -p "$CONFIG_DIR" echo "binary" > "$CONFIG_DIR/install_method" diff --git a/scripts/codetect-wrapper.sh b/scripts/codetect-wrapper.sh index 1dd4e7d..1b03045 100755 --- a/scripts/codetect-wrapper.sh +++ b/scripts/codetect-wrapper.sh @@ -1281,8 +1281,9 @@ cmd_update() { exec brew upgrade codetect ;; go) - info "Installed via go install." - info "Updating codetect-mcp: go install github.com/brian-lai/codetect/cmd/codetect@latest" + info "Installed via go install (updates codetect-mcp only)." + info "For a full update including the wrapper and indexer, use brew or the curl installer." + info "Running: go install github.com/brian-lai/codetect/cmd/codetect@latest" exec go install github.com/brian-lai/codetect/cmd/codetect@latest ;; binary) @@ -1378,15 +1379,19 @@ check_for_updates() { if (( now - last < 86400 )); then return 0 fi - echo "$now" > "$stamp_file" 2>/dev/null || true + # Only write timestamp after a successful API response so a network failure + # doesn't silence the check for 24 hours local latest latest=$(curl -sf --max-time 3 \ "https://api.github.com/repos/brian-lai/codetect/releases/latest" \ 2>/dev/null | grep '"tag_name"' | cut -d'"' -f4) || return 0 [[ -z "$latest" ]] && return 0 - # Get current version tag (strip leading "codetect " prefix if present) + echo "$now" > "$stamp_file" 2>/dev/null || true + # Get current version tag (last word of cmd_version output, e.g. "v3.7.5") local current current=$(cmd_version 2>/dev/null | awk '{print $NF}') || return 0 + # Skip nag for dev builds: their version looks like "(abc1234)" not "vX.Y.Z" + [[ "$current" == "("* ]] && return 0 if [[ -n "$current" && "$latest" != "$current" ]]; then echo -e " ${CYAN}ℹ${NC} codetect $latest is available. Run: codetect update" >&2 fi diff --git a/scripts/install-binary.sh b/scripts/install-binary.sh index 4ce5396..4c7f19a 100755 --- a/scripts/install-binary.sh +++ b/scripts/install-binary.sh @@ -160,12 +160,17 @@ if [[ -d "$TMP_DIR/templates" ]]; then cp -r "$TMP_DIR/templates/." "$SHARE_DIR/templates/" fi -# Store VERSION and a copy of this installer for `codetect update` +# Store VERSION and a copy of this installer for `codetect update`. +# When run via "curl | bash", $0 is /dev/stdin so we re-download instead. echo "$VERSION_NUM" > "$SHARE_DIR/VERSION" -cp "$0" "$SHARE_DIR/install-binary.sh" 2>/dev/null || \ +if [[ -f "$0" && "$0" != "/dev/stdin" && "$0" != "bash" ]]; then + cp "$0" "$SHARE_DIR/install-binary.sh" 2>/dev/null || true +fi +if [[ ! -s "$SHARE_DIR/install-binary.sh" ]]; then curl -fsSL --max-time 10 \ "https://raw.githubusercontent.com/$REPO/main/scripts/install-binary.sh" \ -o "$SHARE_DIR/install-binary.sh" 2>/dev/null || true +fi chmod +x "$SHARE_DIR/install-binary.sh" 2>/dev/null || true # Record install method @@ -187,7 +192,10 @@ if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then else SHELL_RC="$HOME/.bashrc" fi - read -r -p " Add to $SHELL_RC now? [Y/n] " ADD_PATH + # Only prompt if stdin is a terminal — "curl | bash" has no interactive stdin + if [[ -t 0 ]]; then + read -r -p " Add to $SHELL_RC now? [Y/n] " ADD_PATH + fi ADD_PATH=${ADD_PATH:-Y} if [[ $ADD_PATH =~ ^[Yy] ]]; then echo "" >> "$SHELL_RC" From 0447e286e7eef18e19e88890215a33a04d47b6f5 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Mon, 16 Mar 2026 20:12:57 -0700 Subject: [PATCH 10/11] fix: address round-2 review issues - postinst: don't use XDG_CONFIG_HOME under sudo (belongs to root not SUDO_USER) - build-deb.sh: add --abbrev=0 fallback to VERSION detection (consistent with Makefile) - check_for_updates: replace (( )) arithmetic with [[ $(( )) -ge ]] to avoid set -e exit on zero - install-binary.sh: document that non-interactive curl|bash defaults to adding PATH - Formula/codetect.rb: add TODO comment on sha256 placeholder line --- Formula/codetect.rb | 2 +- packaging/deb/DEBIAN/postinst | 6 +++--- packaging/deb/build-deb.sh | 2 +- scripts/codetect-wrapper.sh | 5 ++--- scripts/install-binary.sh | 3 ++- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Formula/codetect.rb b/Formula/codetect.rb index 2486ac2..3bb02ef 100644 --- a/Formula/codetect.rb +++ b/Formula/codetect.rb @@ -14,7 +14,7 @@ class Codetect < Formula desc "Fast, token-efficient codebase search MCP server for Claude Code and any LLM" homepage "https://github.com/brian-lai/codetect" url "https://github.com/brian-lai/codetect/archive/refs/tags/v3.7.5.tar.gz" - sha256 "PLACEHOLDER_SHA256_UPDATE_ON_RELEASE" + sha256 "PLACEHOLDER_SHA256_UPDATE_ON_RELEASE" # TODO: update url, sha256, and version on each release license "MIT" version "3.7.5" diff --git a/packaging/deb/DEBIAN/postinst b/packaging/deb/DEBIAN/postinst index 635165b..2032122 100755 --- a/packaging/deb/DEBIAN/postinst +++ b/packaging/deb/DEBIAN/postinst @@ -10,9 +10,9 @@ else USER_HOME="$HOME" fi -# Record install method for codetect update -XDG_CONFIG="${XDG_CONFIG_HOME:-$USER_HOME/.config}" -CONFIG_DIR="$XDG_CONFIG/codetect" +# Record install method for codetect update. +# Do not use $XDG_CONFIG_HOME here — under sudo it belongs to root, not $SUDO_USER. +CONFIG_DIR="$USER_HOME/.config/codetect" mkdir -p "$CONFIG_DIR" echo "binary" > "$CONFIG_DIR/install_method" diff --git a/packaging/deb/build-deb.sh b/packaging/deb/build-deb.sh index 12f4159..d4f85ff 100755 --- a/packaging/deb/build-deb.sh +++ b/packaging/deb/build-deb.sh @@ -15,7 +15,7 @@ set -e -VERSION="${1:-$(git describe --tags --exact-match 2>/dev/null || echo dev)}" +VERSION="${1:-$(git describe --tags --exact-match 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || echo dev)}" VERSION_NUM="${VERSION#v}" ARCH="${2:-amd64}" diff --git a/scripts/codetect-wrapper.sh b/scripts/codetect-wrapper.sh index 1b03045..5962abe 100755 --- a/scripts/codetect-wrapper.sh +++ b/scripts/codetect-wrapper.sh @@ -1376,9 +1376,8 @@ check_for_updates() { now=$(date +%s 2>/dev/null) || return 0 local last last=$(cat "$stamp_file" 2>/dev/null) || last=0 - if (( now - last < 86400 )); then - return 0 - fi + # Use >= to avoid (( expr == 0 )) returning exit code 1 under set -e + [[ $(( now - last )) -ge 86400 ]] || return 0 # Only write timestamp after a successful API response so a network failure # doesn't silence the check for 24 hours local latest diff --git a/scripts/install-binary.sh b/scripts/install-binary.sh index 4c7f19a..25652df 100755 --- a/scripts/install-binary.sh +++ b/scripts/install-binary.sh @@ -192,7 +192,8 @@ if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then else SHELL_RC="$HOME/.bashrc" fi - # Only prompt if stdin is a terminal — "curl | bash" has no interactive stdin + # Only prompt if stdin is a terminal — non-interactive (curl | bash) defaults + # to adding PATH automatically, consistent with rustup/Homebrew behavior. if [[ -t 0 ]]; then read -r -p " Add to $SHELL_RC now? [Y/n] " ADD_PATH fi From 47bcf46bf0f9417a6ac6b06c55d939c5b77dee54 Mon Sep 17 00:00:00 2001 From: Brian Lai Date: Fri, 20 Mar 2026 09:00:27 -0400 Subject: [PATCH 11/11] test: add Docker integration test suite for install-binary.sh --- scripts/test-docker-install.sh | 261 +++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100755 scripts/test-docker-install.sh diff --git a/scripts/test-docker-install.sh b/scripts/test-docker-install.sh new file mode 100755 index 0000000..2f5fb95 --- /dev/null +++ b/scripts/test-docker-install.sh @@ -0,0 +1,261 @@ +#!/bin/bash +# +# Integration test: verify install-binary.sh behavior in a Docker container. +# +# Strategy: We're testing installer *shell logic*, not the Go binaries. +# The "binaries" in the mock tarball are shell stubs that print version info. +# This lets us test all installer behaviors without needing a cross-compiled build. +# +# Usage (from repo root): +# bash scripts/test-docker-install.sh +# +# Requirements: Docker +# + +set -e + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPTS="$REPO_ROOT/scripts" + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +NC='\033[0m' + +pass() { echo -e "${GREEN}✓ PASS${NC} $1"; } +fail() { echo -e "${RED}✗ FAIL${NC} $1"; FAILURES=$((FAILURES+1)); } +info() { echo -e "${CYAN}→${NC} $1"; } + +FAILURES=0 + +# ── Prerequisites ───────────────────────────────────────────────────────────── + +if ! command -v docker &>/dev/null; then + echo "Docker not found" >&2; exit 1 +fi + +# Build the test image if not already present +if ! docker image inspect codetect-test-env &>/dev/null; then + info "Building test image (Alpine + bash + curl + ripgrep)..." + TMPCTX=$(mktemp -d) + printf 'FROM alpine:3.19\nRUN apk add --no-cache bash curl ripgrep coreutils\n' > "$TMPCTX/Dockerfile" + docker build -q -t codetect-test-env "$TMPCTX" >/dev/null + rm -rf "$TMPCTX" +fi + +# ── Step 1: Build mock release tarball with stub binaries ───────────────────── +# +# Stubs are shell scripts that act as codetect binaries. +# This avoids needing to cross-compile Go for linux/arm64 or linux/amd64. + +info "Building mock release tarball (stub binaries)..." + +VERSION_TAG="v99.0.0-test" +VERSION_NUM="${VERSION_TAG#v}" + +TMP_STAGE=$(mktemp -d) +cleanup() { rm -rf "$TMP_STAGE" "${SERVE_DIR:-}" "${PATCHED:-}"; } +trap cleanup EXIT + +# codetect-mcp stub (MCP server — just needs to exist and be executable) +cat > "$TMP_STAGE/codetect-mcp" <<'EOF' +#!/bin/bash +echo "codetect-mcp stub" +EOF + +# codetect-index stub (reports version) +cat > "$TMP_STAGE/codetect-index" <<'EOF' +#!/bin/bash +if [[ "$1" == "version" ]]; then echo "codetect-index v99.0.0-test"; fi +EOF + +# Other binary stubs +for bin in codetect-daemon codetect-eval migrate-to-postgres; do + echo '#!/bin/bash' > "$TMP_STAGE/$bin" +done + +chmod +x "$TMP_STAGE"/* + +# The wrapper script is the real one from the branch +cp "$SCRIPTS/codetect-wrapper.sh" "$TMP_STAGE/codetect" +chmod +x "$TMP_STAGE/codetect" + +# Templates dir (empty is fine) +mkdir -p "$TMP_STAGE/templates" + +# Detect host arch to match Docker's default platform (Docker uses host arch on Apple Silicon) +case "$(uname -m)" in + arm64|aarch64) TARBALL_ARCH="arm64" ;; + *) TARBALL_ARCH="amd64" ;; +esac +TARBALL_NAME="codetect_linux_${TARBALL_ARCH}.tar.gz" + +SERVE_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_STAGE" "$SERVE_DIR" "${PATCHED:-}"' EXIT + +tar -czf "$SERVE_DIR/$TARBALL_NAME" -C "$TMP_STAGE" . +(cd "$SERVE_DIR" && sha256sum "$TARBALL_NAME" > checksums.txt) + +# Fake GitHub API latest-release endpoint +mkdir -p "$SERVE_DIR/repos/brian-lai/codetect/releases" +printf '{"tag_name":"%s"}\n' "$VERSION_TAG" \ + > "$SERVE_DIR/repos/brian-lai/codetect/releases/latest" + +# Serve tarball + checksums at the expected releases/download path +DOWNLOAD_DIR="$SERVE_DIR/brian-lai/codetect/releases/download/$VERSION_TAG" +mkdir -p "$DOWNLOAD_DIR" +cp "$SERVE_DIR/$TARBALL_NAME" "$DOWNLOAD_DIR/" +cp "$SERVE_DIR/checksums.txt" "$DOWNLOAD_DIR/" + +# Also place under both arch names so the installer finds it regardless +cp "$DOWNLOAD_DIR/$TARBALL_NAME" "$DOWNLOAD_DIR/codetect_linux_amd64.tar.gz" 2>/dev/null || true +cp "$DOWNLOAD_DIR/$TARBALL_NAME" "$DOWNLOAD_DIR/codetect_linux_arm64.tar.gz" 2>/dev/null || true +(cd "$DOWNLOAD_DIR" && sha256sum codetect_linux_*.tar.gz > checksums.txt) + +cp "$SCRIPTS/install-binary.sh" "$SERVE_DIR/install-binary.sh" + +echo " Tarball arch: linux/$TARBALL_ARCH ($(du -sh "$SERVE_DIR/$TARBALL_NAME" | cut -f1))" + +# ── Step 2: Start local HTTP server ────────────────────────────────────────── + +HTTP_PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()") +info "Starting local HTTP server on port $HTTP_PORT..." + +python3 -m http.server "$HTTP_PORT" --directory "$SERVE_DIR" >/tmp/codetect-test-http.log 2>&1 & +HTTP_PID=$! +trap 'kill $HTTP_PID 2>/dev/null; rm -rf "$TMP_STAGE" "$SERVE_DIR" "${PATCHED:-}"' EXIT +sleep 1 + +curl -sf "http://localhost:$HTTP_PORT/$TARBALL_NAME" -o /dev/null \ + || { echo "HTTP server failed to start:" >&2; cat /tmp/codetect-test-http.log >&2; exit 1; } +echo " Serving on http://localhost:$HTTP_PORT" + +# ── Step 3: Patch installer URLs to use local server ───────────────────────── + +HOST="host.docker.internal" +PATCHED=$(mktemp /tmp/install-binary-test-XXXX.sh) +trap 'kill $HTTP_PID 2>/dev/null; rm -rf "$TMP_STAGE" "$SERVE_DIR" "$PATCHED"' EXIT + +sed \ + -e "s|https://api.github.com/repos/\$REPO/releases/latest|http://$HOST:$HTTP_PORT/repos/brian-lai/codetect/releases/latest|g" \ + -e "s|https://github.com/\$REPO/releases/download/\${VERSION_TAG}/\${TARBALL}|http://$HOST:$HTTP_PORT/brian-lai/codetect/releases/download/\${VERSION_TAG}/\${TARBALL}|g" \ + -e "s|https://github.com/\$REPO/releases/download/\${VERSION_TAG}/checksums.txt|http://$HOST:$HTTP_PORT/brian-lai/codetect/releases/download/\${VERSION_TAG}/checksums.txt|g" \ + -e "s|https://raw.githubusercontent.com/\$REPO/main/scripts/install-binary.sh|http://$HOST:$HTTP_PORT/install-binary.sh|g" \ + "$SCRIPTS/install-binary.sh" > "$PATCHED" +chmod +x "$PATCHED" + +# ── Step 4: Run tests ───────────────────────────────────────────────────────── + +echo "" +info "Running tests in Ubuntu 22.04 container..." +echo "" + +# Helper: run a command in a fresh container (codetect-test-env = Alpine + bash + curl + ripgrep) +docker_run() { + docker run --rm \ + --add-host=host.docker.internal:host-gateway \ + -v "$PATCHED:/install-binary.sh:ro" \ + -e HOME=/root \ + codetect-test-env \ + bash -c "$1" 2>&1 +} + +# Test 1: Install completes +info "Test 1: install completes without error" +OUT=$(docker_run "bash /install-binary.sh") +if echo "$OUT" | grep -q "Installed codetect"; then + pass "install completes, success message present" +else + fail "install did not complete cleanly" + echo "$OUT" | tail -15 +fi + +# Test 2: install_method marker = "binary" +info "Test 2: install_method marker = 'binary'" +OUT=$(docker_run "bash /install-binary.sh 2>/dev/null; cat \$HOME/.config/codetect/install_method") +if echo "$OUT" | grep -q "^binary$"; then + pass "install_method = 'binary'" +else + fail "install_method wrong (got: $(echo "$OUT" | tail -1))" +fi + +# Test 3: VERSION file written correctly +info "Test 3: VERSION file written with correct version" +OUT=$(docker_run "bash /install-binary.sh 2>/dev/null; cat \$HOME/.local/share/codetect/VERSION") +if echo "$OUT" | grep -q "^${VERSION_NUM}$"; then + pass "VERSION = $VERSION_NUM" +else + fail "VERSION file wrong (got: $(echo "$OUT" | tail -1))" +fi + +# Test 4: codetect binary is in place and executable +info "Test 4: codetect wrapper is installed and executable" +OUT=$(docker_run "bash /install-binary.sh 2>/dev/null; test -x \$HOME/.local/bin/codetect && echo EXECUTABLE || echo NOT_FOUND") +if echo "$OUT" | grep -q "^EXECUTABLE$"; then + pass "codetect wrapper is executable at ~/.local/bin/codetect" +else + fail "codetect wrapper not found or not executable" +fi + +# Test 5: Checksum verification ran +info "Test 5: checksum verification runs" +OUT=$(docker_run "bash /install-binary.sh 2>&1") +if echo "$OUT" | grep -qi "checksum verified\|verifying checksum"; then + pass "checksum verification ran" +else + fail "no evidence of checksum verification" + echo "$OUT" | grep -i check || true +fi + +# Test 6: Update check suppressed by fresh stamp file +info "Test 6: update nag suppressed when stamp file is fresh" +OUT=$(docker_run " + bash /install-binary.sh 2>/dev/null + export PATH=\$HOME/.local/bin:\$PATH + date +%s > \$HOME/.config/codetect/last_update_check + codetect version 2>&1 +") +if echo "$OUT" | grep -q "is available"; then + fail "update nag shown despite fresh stamp" +else + pass "update nag suppressed by fresh stamp file" +fi + +# Test 7: codetect update delegates to install-binary.sh +info "Test 7: 'codetect update' delegates to install-binary.sh" +OUT=$(docker_run " + bash /install-binary.sh 2>/dev/null + export PATH=\$HOME/.local/bin:\$PATH + printf '#!/bin/bash\necho UPDATER_CALLED\n' > \$HOME/.local/share/codetect/install-binary.sh + chmod +x \$HOME/.local/share/codetect/install-binary.sh + codetect update 2>&1 +") +if echo "$OUT" | grep -q "UPDATER_CALLED"; then + pass "codetect update delegates to install-binary.sh" +else + fail "codetect update did not call install-binary.sh" + echo "$OUT" | tail -5 +fi + +# Test 8: Non-interactive PATH written to .bashrc +info "Test 8: PATH written to .bashrc in non-interactive mode" +OUT=$(docker_run " + bash /install-binary.sh 2>/dev/null + grep -c 'local/bin' \$HOME/.bashrc 2>/dev/null || echo 0 +") +MATCH=$(echo "$OUT" | grep -E '^[0-9]+$' | tail -1) +if [[ "${MATCH:-0}" -ge 1 ]]; then + pass "PATH line written to .bashrc in non-interactive mode" +else + fail "PATH not written to .bashrc" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── + +echo "" +if [[ $FAILURES -eq 0 ]]; then + echo -e "${GREEN}All 8 tests passed.${NC}" +else + echo -e "${RED}$FAILURES / 8 test(s) failed.${NC}" + exit 1 +fi