diff --git a/.local-binaries.txt b/.local-binaries.txt index bb18292de..086d86e91 100644 --- a/.local-binaries.txt +++ b/.local-binaries.txt @@ -8,4 +8,4 @@ ~/ghq/github.com/Dicklesworthstone/ultimate_bug_scanner/ubs ~/ghq/github.com/nwiizo/ccswarm/target/release/ccswarm ~/ghq/github.com/steveyegge/beads/bd -~/ghq/github.com/steveyegge/gastown +~/ghq/github.com/steveyegge/gastown/gt diff --git a/Makefile b/Makefile index d0c8243d1..8646ae83b 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,7 @@ help: ## Show this help message. ##@ General .PHONY: install -install: setup nix-flake-update nix-build nix-switch shell-install ## Set up full environment (setup, flake-update, build, switch, shell-install). +install: setup nix-build nix-switch shell-install ## Set up full environment (setup, flake-update, build, switch, shell-install). .PHONY: build build: nix-build ## Build Nix configuration. @@ -159,26 +159,25 @@ sync: ## Sync dotagents (commands, skills, MCP configuration). .PHONY: test test: neovim-test shell-test ## Run all tests (neovim + shell). -.PHONY: update -update: nix-update shell-update neovim-update ## Update Nix flake and configurations. - -##@ Upgrade +##@ Update -.PHONY: upgrade -upgrade: upgrade-overlays neovim-upgrade ## Upgrade overlays and Nix flake, then rebuild. +.PHONY: update +update: update-local-binaries ## Update local binaries -.PHONY: upgrade-overlays -upgrade-overlays: ## Upgrade all custom overlays to latest versions. - @echo "🔄 Upgrading custom overlays..." +.PHONY: update-local-binaries +update-local-binaries: ## Update and rebuild local binaries from .local-binaries.txt. + @echo "🔄 Updating local binaries..." @if [ "$$CI" = "true" ] || [ "$$IN_DOCKER" = "true" ]; then \ - echo "⏭️ Skipping overlay upgrade in CI/Docker"; \ + echo "⏭️ Skipping local binaries update in CI/Docker"; \ else \ - ./scripts/upgrade-overlays.sh all; \ + ./scripts/update-local-binaries.sh; \ fi -.PHONY: upgrade-clawdbot -upgrade-clawdbot: ## Upgrade clawdbot overlay to latest release. - @./scripts/upgrade-overlays.sh clawdbot + +##@ Upgrade + +.PHONY: upgrade +upgrade: nix-upgrade overlays-upgrade neovim-upgrade ## Upgrade Nix flake, overlays, Neovim plugins .PHONY: dev dev: nix-develop ## Enter the Nix dev shell (alias for nix-develop). @@ -249,8 +248,8 @@ nix-install: ## Install Nix if not already installed. ##@ Nix -.PHONY: nix-update -nix-update: nix-flake-update nix-build nix-switch ## Update Nix flake, build, and switch. +.PHONY: nix-upgrade +nix-upgrade: nix-flake-upgrade nix-build nix-switch ## Upgrade Nix flake, build, and switch. .PHONY: nix-backup nix-backup: ## Backup configuration files. @@ -304,8 +303,8 @@ nix-flake-check: ## Check Nix flake configuration. fi @echo "✅ Nix flake check completed successfully" -.PHONY: nix-flake-update -nix-flake-update: nix-connect ## Update flake.lock file. +.PHONY: nix-flake-upgrade +nix-flake-upgrade: nix-connect ## Update flake.lock file. @echo "♻️ Refreshing flake.lock file..." @if [ "$$CI" = "true" ] || [ "$$IN_DOCKER" = "true" ] || [ "$$AUTOMATED_UPDATE" = "true" ]; then \ echo "Bypassing flake update in CI/Docker/automated update"; \ @@ -509,8 +508,6 @@ shell-install: ## Set up Fish shell as default shell. echo "⚠️ Fish shell not found. Skipping Fish setup."; \ fi -.PHONY: shell-update -shell-update: shell-install ## Update Fish shell setup (alias for shell-install). ##@ Docker @@ -756,7 +753,7 @@ shell-test-dev: ## Run shell tests inside the Nix dev shell (mirrors CI). .PHONY: shell-check shell-check: ## Run ShellCheck on shell scripts. @echo "🔍 Running ShellCheck..." - @find . -name '*.sh' -not -path './node_modules/*' -not -path './.git/*' -not -path './result/*' -not -path './.venv/*' | xargs shellcheck + @find . -name '*.sh' -not -path './node_modules/*' -not -path './.git/*' -not -path './result/*' -not -path './.venv/*' -not -path './.claude/*' -not -path './dotagents/.claude/*' -not -path './dotagents/.codex/*' -print0 | xargs -0 shellcheck .PHONY: shell-check-dev shell-check-dev: ## Run ShellCheck inside the Nix dev shell (mirrors CI). @@ -779,3 +776,14 @@ doppler-upload: ## Upload .env file to Doppler (dotfiles/prd). @echo "🔐 Uploading .env to Doppler..." @doppler secrets upload --project dotfiles --config prd .env @echo "✅ .env uploaded to Doppler (dotfiles/prd)" + +##@ Overlays + +.PHONY: overlays-upgrade +overlays-upgrade: ## Upgrade all custom overlays to latest versions. + @echo "🔄 Upgrading custom overlays..." + @if [ "$$CI" = "true" ] || [ "$$IN_DOCKER" = "true" ]; then \ + echo "⏭️ Skipping overlay upgrade in CI/Docker"; \ + else \ + ./scripts/upgrade-overlays.sh all; \ + fi diff --git a/home-manager/services/default.nix b/home-manager/services/default.nix index c4899945b..13465c073 100644 --- a/home-manager/services/default.nix +++ b/home-manager/services/default.nix @@ -12,6 +12,7 @@ let codeSyncer = import ./code-syncer { inherit pkgs; }; docker = import ./docker { inherit lib pkgs; }; dotfilesUpdater = import ./dotfiles-updater { inherit pkgs; }; + makeUpdater = import ./make-updater { inherit pkgs; }; neversslKeepalive = import ./neverssl-keepalive { inherit pkgs; }; ollama = import ./ollama { inherit pkgs; }; sshAgent = import ./ssh-agent { @@ -24,6 +25,7 @@ in codeSyncer docker dotfilesUpdater + makeUpdater neversslKeepalive ollama sshAgent diff --git a/home-manager/services/make-updater/default.nix b/home-manager/services/make-updater/default.nix new file mode 100644 index 000000000..15e61903c --- /dev/null +++ b/home-manager/services/make-updater/default.nix @@ -0,0 +1,68 @@ +{ pkgs, ... }: +let + inherit (pkgs) lib; +in +{ + launchd.agents.make-updater = lib.mkIf pkgs.stdenv.isDarwin { + enable = true; + config = { + ProgramArguments = [ + "${pkgs.bash}/bin/bash" + "${./update.sh}" + ]; + Environment = { + PATH = "${ + lib.makeBinPath [ + pkgs.git + pkgs.bash + pkgs.coreutils + pkgs.gnumake + ] + }:/opt/homebrew/bin:/usr/local/bin"; + }; + StartInterval = 10800; + StandardOutPath = "/tmp/make-updater.log"; + StandardErrorPath = "/tmp/make-updater.error.log"; + }; + }; + + systemd.user.services.make-updater = lib.mkIf pkgs.stdenv.isLinux { + Unit = { + Description = "Make update service"; + }; + Service = { + Type = "oneshot"; + Environment = [ + "PATH=${ + lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.gawk + pkgs.git + pkgs.gnumake + pkgs.gnused + pkgs.nix + pkgs.sudo + pkgs.which + ] + }" + "AUTOMATED_UPDATE=true" + ]; + ExecStart = "${./update.sh}"; + }; + }; + + systemd.user.timers.make-updater = lib.mkIf pkgs.stdenv.isLinux { + Unit = { + Description = "Timer for make update"; + }; + Timer = { + OnCalendar = "*-*-* 00/3:00:00"; + Persistent = true; + }; + Install = { + WantedBy = [ "timers.target" ]; + }; + }; +} diff --git a/home-manager/services/make-updater/update.sh b/home-manager/services/make-updater/update.sh new file mode 100755 index 000000000..0b813f0d9 --- /dev/null +++ b/home-manager/services/make-updater/update.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd ~/dotfiles + +make update diff --git a/scripts/update-local-binaries.sh b/scripts/update-local-binaries.sh new file mode 100755 index 000000000..9d90c086d --- /dev/null +++ b/scripts/update-local-binaries.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# Update and build all local binaries listed in .local-binaries.txt +# Usage: ./scripts/update-local-binaries.sh [filter] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_FILE="$REPO_ROOT/.local-binaries.txt" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Track results +declare -a SUCCESSES=() +declare -a FAILURES=() + +log_info() { + echo -e "${GREEN}$1${NC}" +} + +log_warn() { + echo -e "${YELLOW}$1${NC}" +} + +log_error() { + echo -e "${RED}$1${NC}" +} + +log_step() { + echo -e "${BLUE}$1${NC}" +} + +get_ghq_root() { + if command -v ghq >/dev/null 2>&1; then + ghq root + else + echo "$HOME/ghq" + fi +} + +get_ghq_repo() { + local repo_dir="$1" + local ghq_root + ghq_root="$(get_ghq_root)" + + case "$repo_dir" in + "$ghq_root"/*) + echo "${repo_dir#"$ghq_root/"}" + return 0 + ;; + esac + + return 1 +} + +# Extract repo directory from binary path +# e.g., ~/ghq/github.com/owner/repo/target/release/bin -> ~/ghq/github.com/owner/repo +get_repo_dir() { + local binary_path="$1" + # Expand ~ to $HOME + binary_path="${binary_path/#\~/$HOME}" + + # Find the repo root by looking for .git directory + local dir + dir="$(dirname "$binary_path")" + + while [ "$dir" != "/" ] && [ "$dir" != "$HOME" ]; do + if [ -d "$dir/.git" ]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + + # Fallback: assume 4 levels deep from ghq root (github.com/owner/repo) + echo "$binary_path" | sed -E 's|(~/ghq/[^/]+/[^/]+/[^/]+)/.*|\1|' | sed "s|~|$HOME|" +} + +# Get repo name for display +get_repo_name() { + local repo_dir="$1" + basename "$repo_dir" +} + +# Detect and run build command +build_repo() { + local repo_dir="$1" + local repo_name + repo_name="$(get_repo_name "$repo_dir")" + + log_step " Building $repo_name..." + + if [ -f "$repo_dir/Makefile" ]; then + if make -C "$repo_dir" build 2>&1; then + return 0 + else + return 1 + fi + elif [ -f "$repo_dir/Cargo.toml" ]; then + if (cd "$repo_dir" && cargo build --release 2>&1); then + return 0 + else + return 1 + fi + else + log_warn " No Makefile or Cargo.toml found, skipping build" + return 0 + fi +} + +# Update a single repo +update_repo() { + local binary_path="$1" + local repo_dir + repo_dir="$(get_repo_dir "$binary_path")" + local repo_name + repo_name="$(get_repo_name "$repo_dir")" + + echo "" + log_info "📦 Updating $repo_name" + echo " Path: $repo_dir" + + if [ ! -d "$repo_dir" ]; then + local ghq_repo + ghq_repo="$(get_ghq_repo "$repo_dir" || true)" + + if [ -n "$ghq_repo" ] && command -v ghq >/dev/null 2>&1; then + log_step " Repo missing; cloning with ghq..." + if ! ghq get "$ghq_repo" 2>&1; then + log_error " ghq clone failed for $ghq_repo" + FAILURES+=("$repo_name (ghq clone failed)") + return 1 + fi + else + log_error " Directory not found: $repo_dir" + FAILURES+=("$repo_name (not found)") + return 1 + fi + fi + + # Git pull + log_step " Pulling latest changes..." + if ! (cd "$repo_dir" && git pull 2>&1); then + log_error " Git pull failed" + FAILURES+=("$repo_name (git pull failed)") + return 1 + fi + + # Build + if build_repo "$repo_dir"; then + log_info " ✅ $repo_name updated successfully" + SUCCESSES+=("$repo_name") + else + log_error " ❌ Build failed for $repo_name" + FAILURES+=("$repo_name (build failed)") + return 1 + fi +} + +# Print summary +print_summary() { + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_info "Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ ${#SUCCESSES[@]} -gt 0 ]; then + echo -e "${GREEN}✅ Successful (${#SUCCESSES[@]}):${NC}" + for name in "${SUCCESSES[@]}"; do + echo " - $name" + done + fi + + if [ ${#FAILURES[@]} -gt 0 ]; then + echo -e "${RED}❌ Failed (${#FAILURES[@]}):${NC}" + for name in "${FAILURES[@]}"; do + echo " - $name" + done + fi +} + +usage() { + echo "Usage: $0 [filter]" + echo "" + echo "Update and build local binaries from .local-binaries.txt" + echo "" + echo "Arguments:" + echo " filter Optional: Only update repos matching this pattern" + echo "" + echo "Examples:" + echo " $0 # Update all" + echo " $0 beads # Update repos containing 'beads'" +} + +main() { + local filter="${1:-}" + + if [ "$filter" = "-h" ] || [ "$filter" = "--help" ]; then + usage + exit 0 + fi + + if [ ! -f "$CONFIG_FILE" ]; then + log_error "Config file not found: $CONFIG_FILE" + exit 1 + fi + + log_info "🔄 Updating local binaries" + [ -n "$filter" ] && echo " Filter: $filter" + + # Track unique repos to avoid duplicate builds + declare -A seen_repos + + while IFS= read -r line || [ -n "$line" ]; do + # Skip comments and empty lines + [[ $line =~ ^[[:space:]]*# ]] && continue + [[ -z ${line// /} ]] && continue + + # Apply filter if provided + if [ -n "$filter" ] && [[ ! $line =~ $filter ]]; then + continue + fi + + # Get repo directory and check if already processed + local repo_dir + repo_dir="$(get_repo_dir "$line")" + + if [ -n "${seen_repos[$repo_dir]:-}" ]; then + continue + fi + seen_repos[$repo_dir]=1 + + # Update repo (continue on failure) + update_repo "$line" || true + + done <"$CONFIG_FILE" + + print_summary + + # Exit with error if any failures + if [ ${#FAILURES[@]} -gt 0 ]; then + exit 1 + fi +} + +main "$@" diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index af8da1a01..2e7d9546e 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -49,6 +49,10 @@ It 'has spec file for home-manager/services/dotfiles-updater/update.sh' The path "spec/dotfiles_updater_spec.sh" should be exist End +It 'has spec file for home-manager/services/make-updater/update.sh' +The path "spec/make_updater_spec.sh" should be exist +End + It 'has spec file for home-manager/services/neverssl-keepalive/keepalive.sh' The path "spec/keepalive_spec.sh" should be exist End @@ -69,6 +73,10 @@ It 'has spec file for scripts/update-gitalias.sh' The path "spec/update_gitalias_spec.sh" should be exist End +It 'has spec file for scripts/update-local-binaries.sh' +The path "spec/update_local_binaries_spec.sh" should be exist +End + It 'has spec file for scripts/upgrade-overlays.sh' The path "spec/upgrade_overlays_spec.sh" should be exist End @@ -129,11 +137,13 @@ home-manager/services/cliproxyapi/scripts/start.sh home-manager/services/cliproxyapi/scripts/wrapper.sh home-manager/services/code-syncer/sync.sh home-manager/services/dotfiles-updater/update.sh +home-manager/services/make-updater/update.sh home-manager/services/neverssl-keepalive/keepalive.sh install.sh named-hosts/kyber/rekey-galactica.sh named-hosts/kyber/setup.sh scripts/update-gitalias.sh +scripts/update-local-binaries.sh scripts/upgrade-overlays.sh" # Get actual scripts from git (excluding spec directory) diff --git a/spec/make_updater_spec.sh b/spec/make_updater_spec.sh new file mode 100644 index 000000000..914d79933 --- /dev/null +++ b/spec/make_updater_spec.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'make-updater/update.sh' +SCRIPT="$PWD/home-manager/services/make-updater/update.sh" + +Describe 'script properties' +It 'uses bash shebang' +When run bash -c "head -1 '$SCRIPT'" +The output should include '#!/usr/bin/env bash' +End + +It 'uses strict mode' +When run bash -c "head -5 '$SCRIPT'" +The output should include 'set -euo pipefail' +End + +It 'changes to ~/dotfiles directory' +When run bash -c "grep 'cd ~/dotfiles' '$SCRIPT'" +The output should include 'cd ~/dotfiles' +End +End + +Describe 'make update execution' +It 'runs make update' +When run bash -c "grep 'make update' '$SCRIPT'" +The output should include 'make update' +End +End + +End diff --git a/spec/update_local_binaries_spec.sh b/spec/update_local_binaries_spec.sh new file mode 100644 index 000000000..a7bb929f4 --- /dev/null +++ b/spec/update_local_binaries_spec.sh @@ -0,0 +1,487 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329,SC2034 + +Describe 'scripts/update-local-binaries.sh' +SCRIPT="$PWD/scripts/update-local-binaries.sh" + +Describe 'script properties' + It 'uses bash shebang' + When run bash -c "head -1 '$SCRIPT'" + The output should include '#!/usr/bin/env bash' + End + + It 'uses strict mode' + When run bash -c "head -10 '$SCRIPT'" + The output should include 'set -euo pipefail' + End +End + +Describe 'configuration' + It 'reads from .local-binaries.txt' + When run bash -c "grep 'CONFIG_FILE=' '$SCRIPT'" + The output should include '.local-binaries.txt' + End + + It 'calculates script directory' + When run bash -c "grep 'SCRIPT_DIR=' '$SCRIPT'" + The output should include 'SCRIPT_DIR=' + End + + It 'calculates repo root' + When run bash -c "grep 'REPO_ROOT=' '$SCRIPT'" + The output should include 'REPO_ROOT=' + End +End + +Describe 'color definitions' + It 'defines RED color' + When run bash -c "grep 'RED=' '$SCRIPT'" + The output should include 'RED=' + End + + It 'defines GREEN color' + When run bash -c "grep 'GREEN=' '$SCRIPT'" + The output should include 'GREEN=' + End + + It 'defines YELLOW color' + When run bash -c "grep 'YELLOW=' '$SCRIPT'" + The output should include 'YELLOW=' + End + + It 'defines BLUE color' + When run bash -c "grep 'BLUE=' '$SCRIPT'" + The output should include 'BLUE=' + End + + It 'defines NC (no color)' + When run bash -c "grep 'NC=' '$SCRIPT'" + The output should include 'NC=' + End +End + +Describe 'logging functions' + It 'has log_info function' + When run bash -c "grep 'log_info()' '$SCRIPT'" + The output should include 'log_info()' + End + + It 'has log_warn function' + When run bash -c "grep 'log_warn()' '$SCRIPT'" + The output should include 'log_warn()' + End + + It 'has log_error function' + When run bash -c "grep 'log_error()' '$SCRIPT'" + The output should include 'log_error()' + End + + It 'has log_step function' + When run bash -c "grep 'log_step()' '$SCRIPT'" + The output should include 'log_step()' + End +End + +Describe 'helper functions' + It 'has get_repo_dir function' + When run bash -c "grep 'get_repo_dir()' '$SCRIPT'" + The output should include 'get_repo_dir()' + End + + It 'has get_ghq_root function' + When run bash -c "grep 'get_ghq_root()' '$SCRIPT'" + The output should include 'get_ghq_root()' + End + + It 'has get_ghq_repo function' + When run bash -c "grep 'get_ghq_repo()' '$SCRIPT'" + The output should include 'get_ghq_repo()' + End + + It 'has get_repo_name function' + When run bash -c "grep 'get_repo_name()' '$SCRIPT'" + The output should include 'get_repo_name()' + End + + It 'has build_repo function' + When run bash -c "grep 'build_repo()' '$SCRIPT'" + The output should include 'build_repo()' + End + + It 'has update_repo function' + When run bash -c "grep 'update_repo()' '$SCRIPT'" + The output should include 'update_repo()' + End + + It 'has print_summary function' + When run bash -c "grep 'print_summary()' '$SCRIPT'" + The output should include 'print_summary()' + End + + It 'has usage function' + When run bash -c "grep 'usage()' '$SCRIPT'" + The output should include 'usage()' + End +End + +Describe 'repo directory detection' + It 'expands tilde to HOME' + When run bash -c "grep '\$HOME' '$SCRIPT'" + The output should include 'HOME' + End + + It 'uses ghq root when available' + When run bash -c "grep 'ghq root' '$SCRIPT'" + The output should include 'ghq root' + End + + It 'extracts ghq repo paths' + When run bash -c "grep 'ghq_root' '$SCRIPT'" + The output should include 'ghq_root' + End + + It 'detects .git directory for repo root' + When run bash -c "grep '\.git' '$SCRIPT'" + The output should include '.git' + End + + It 'uses basename for repo name' + When run bash -c "grep 'basename' '$SCRIPT'" + The output should include 'basename' + End +End + +Describe 'build detection' + It 'detects Makefile for make build' + When run bash -c "grep 'Makefile' '$SCRIPT'" + The output should include 'Makefile' + End + + It 'detects Cargo.toml for cargo build' + When run bash -c "grep 'Cargo.toml' '$SCRIPT'" + The output should include 'Cargo.toml' + End + + It 'runs cargo build --release for Rust projects' + When run bash -c "grep 'cargo build --release' '$SCRIPT'" + The output should include 'cargo build --release' + End +End + +Describe 'git operations' + It 'performs git pull' + When run bash -c "grep 'git pull' '$SCRIPT'" + The output should include 'git pull' + End + + It 'clones missing repos with ghq' + When run bash -c "grep 'ghq get' '$SCRIPT'" + The output should include 'ghq get' + End +End + +Describe 'result tracking' + It 'tracks successes' + When run bash -c "grep 'SUCCESSES' '$SCRIPT'" + The output should include 'SUCCESSES' + End + + It 'tracks failures' + When run bash -c "grep 'FAILURES' '$SCRIPT'" + The output should include 'FAILURES' + End +End + +Describe 'line processing' + It 'skips comment lines' + When run bash -c "grep -E 'Skip comments|^#' '$SCRIPT'" + The output should include '#' + End + + It 'skips empty lines' + When run bash -c "grep '\-z' '$SCRIPT'" + The output should include '-z' + End +End + +Describe 'duplicate detection' + It 'tracks seen repos' + When run bash -c "grep 'seen_repos' '$SCRIPT'" + The output should include 'seen_repos' + End +End + +Describe 'usage and help' + It 'shows usage with -h flag' + When run bash "$SCRIPT" -h + The output should include 'Usage:' + The output should include 'filter' + The status should be success + End + + It 'shows usage with --help flag' + When run bash "$SCRIPT" --help + The output should include 'Usage:' + The output should include 'Examples:' + The status should be success + End + + It 'shows filter argument documentation' + When run bash "$SCRIPT" --help + The output should include 'Only update repos matching this pattern' + End +End + +Describe 'config file handling' + setup() { + TEMP_DIR=$(mktemp -d) + TEMP_SCRIPT="$TEMP_DIR/test-config.sh" + cat >"$TEMP_SCRIPT" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +CONFIG_FILE="/nonexistent/path/to/.local-binaries.txt" + +RED='\033[0;31m' +NC='\033[0m' + +log_error() { + echo -e "${RED}$1${NC}" +} + +if [ ! -f "$CONFIG_FILE" ]; then + log_error "Config file not found: $CONFIG_FILE" + exit 1 +fi +SCRIPT + chmod +x "$TEMP_SCRIPT" + } + + cleanup() { + rm -rf "$TEMP_DIR" + } + + Before 'setup' + After 'cleanup' + + It 'exits with error when config file not found' + When run bash "$TEMP_SCRIPT" + The output should include 'Config file not found' + The status should be failure + End +End + +Describe 'get_repo_dir function' + setup() { + TEMP_DIR=$(mktemp -d) + TEMP_SCRIPT="$TEMP_DIR/test-repo-dir.sh" + cat >"$TEMP_SCRIPT" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +get_repo_dir() { + local binary_path="$1" + # Expand ~ to $HOME + binary_path="${binary_path/#\~/$HOME}" + + # Find the repo root by looking for .git directory + local dir + dir="$(dirname "$binary_path")" + + while [ "$dir" != "/" ] && [ "$dir" != "$HOME" ]; do + if [ -d "$dir/.git" ]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + + # Fallback: assume 4 levels deep from ghq root (github.com/owner/repo) + echo "$binary_path" | sed -E 's|(~/ghq/[^/]+/[^/]+/[^/]+)/.*|\1|' | sed "s|~|$HOME|" +} + +# Test with actual path +get_repo_dir "$1" +SCRIPT + chmod +x "$TEMP_SCRIPT" + + # Create a mock git repo structure + mkdir -p "$TEMP_DIR/ghq/github.com/owner/repo/.git" + mkdir -p "$TEMP_DIR/ghq/github.com/owner/repo/target/release" + touch "$TEMP_DIR/ghq/github.com/owner/repo/target/release/mybin" + } + + cleanup() { + rm -rf "$TEMP_DIR" + } + + Before 'setup' + After 'cleanup' + + It 'finds repo root by .git directory' + When run bash "$TEMP_SCRIPT" "$TEMP_DIR/ghq/github.com/owner/repo/target/release/mybin" + The output should eq "$TEMP_DIR/ghq/github.com/owner/repo" + End +End + +Describe 'build_repo function behavior' + setup() { + TEMP_DIR=$(mktemp -d) + TEMP_SCRIPT="$TEMP_DIR/test-build.sh" + cat >"$TEMP_SCRIPT" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_warn() { + echo -e "${YELLOW}$1${NC}" +} + +log_step() { + echo -e "${BLUE}$1${NC}" +} + +get_repo_name() { + local repo_dir="$1" + basename "$repo_dir" +} + +build_repo() { + local repo_dir="$1" + local repo_name + repo_name="$(get_repo_name "$repo_dir")" + + log_step " Building $repo_name..." + + if [ -f "$repo_dir/Makefile" ]; then + echo "Would run: make -C $repo_dir build" + return 0 + elif [ -f "$repo_dir/Cargo.toml" ]; then + echo "Would run: cargo build --release" + return 0 + else + log_warn " No Makefile or Cargo.toml found, skipping build" + return 0 + fi +} + +build_repo "$1" +SCRIPT + chmod +x "$TEMP_SCRIPT" + } + + cleanup() { + rm -rf "$TEMP_DIR" + } + + Before 'setup' + After 'cleanup' + + It 'detects Makefile and uses make' + mkdir -p "$TEMP_DIR/repo" + touch "$TEMP_DIR/repo/Makefile" + When run bash "$TEMP_SCRIPT" "$TEMP_DIR/repo" + The output should include 'Building repo' + The output should include 'make' + End + + It 'detects Cargo.toml and uses cargo' + mkdir -p "$TEMP_DIR/rust-repo" + touch "$TEMP_DIR/rust-repo/Cargo.toml" + When run bash "$TEMP_SCRIPT" "$TEMP_DIR/rust-repo" + The output should include 'Building rust-repo' + The output should include 'cargo build --release' + End + + It 'warns when no build system found' + mkdir -p "$TEMP_DIR/unknown-repo" + When run bash "$TEMP_SCRIPT" "$TEMP_DIR/unknown-repo" + The output should include 'No Makefile or Cargo.toml found' + End +End + +Describe 'filter functionality' + setup() { + TEMP_DIR=$(mktemp -d) + CONFIG_FILE="$TEMP_DIR/.local-binaries.txt" + cat >"$CONFIG_FILE" <<'CONFIG' +# Test binaries +~/ghq/github.com/owner/beads/bd +~/ghq/github.com/owner/other-tool/ot +~/ghq/github.com/owner/beads-viewer/bv +CONFIG + + TEMP_SCRIPT="$TEMP_DIR/test-filter.sh" + cat >"$TEMP_SCRIPT" <