blue/install.sh
Eric Garcia c745d11870 feat: RFC 0060 reliable binary installation
- install.sh: Add xattr/codesign fix after cp on macOS
- install.sh: Add portable timeout verification using perl
- INSTALL.md: Recommend cargo install --path as primary method
- INSTALL.md: Document macOS signature issue and fix
- blue doctor: Detect com.apple.provenance xattr with fix hint
- blue doctor: Verify code signature with codesign --verify
- blue doctor: 3-second liveness timeout for hanging binaries

Fixes dyld hang at _dyld_start when copied binaries have stale
signatures after Homebrew library updates.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 21:01:19 -05:00

186 lines
6.1 KiB
Bash
Executable file

#!/bin/bash
# Install Blue CLI to system path
set -e
# Default install location
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
echo "Building Blue (release)..."
cargo build --release
BINARY="target/release/blue"
if [ ! -f "$BINARY" ]; then
echo -e "${RED}Build failed - binary not found${NC}"
exit 1
fi
echo "Installing to $INSTALL_DIR..."
if [ -w "$INSTALL_DIR" ]; then
cp "$BINARY" "$INSTALL_DIR/blue"
else
echo "Need sudo for $INSTALL_DIR"
sudo cp "$BINARY" "$INSTALL_DIR/blue"
fi
# RFC 0060: Fix macOS code signature after copy
# cp preserves stale xattrs and adhoc signatures, causing dyld hangs
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "Fixing macOS code signature..."
if [ -w "$INSTALL_DIR/blue" ]; then
xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true
codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true
else
sudo xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true
sudo codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true
fi
fi
# Verify installation with timeout (RFC 0060)
# A hanging binary won't respond in 3 seconds
# Note: Using perl for portable timeout (works on macOS and Linux)
run_with_timeout() {
local timeout_sec=$1
shift
perl -e 'alarm shift; exec @ARGV' "$timeout_sec" "$@" 2>/dev/null
}
echo "Verifying installation..."
BLUE_PATH="$INSTALL_DIR/blue"
if [ -x "$BLUE_PATH" ]; then
if run_with_timeout 3 "$BLUE_PATH" --version >/dev/null 2>&1; then
echo -e "${GREEN}Installed successfully${NC}"
"$BLUE_PATH" --version 2>/dev/null || echo "blue installed to $BLUE_PATH"
else
echo -e "${RED}Binary installed but failed verification (possible signing issue)${NC}"
echo "Try: xattr -cr $BLUE_PATH && codesign --force --sign - $BLUE_PATH"
exit 1
fi
elif command -v blue &> /dev/null; then
if run_with_timeout 3 blue --version >/dev/null 2>&1; then
echo -e "${GREEN}Installed successfully${NC}"
blue --version 2>/dev/null || echo "blue available in PATH"
else
echo -e "${RED}Binary in PATH but failed verification${NC}"
exit 1
fi
else
echo -e "${GREEN}Installed to $BLUE_PATH${NC}"
echo "Add $INSTALL_DIR to PATH if not already present"
fi
# Update MCP config if it exists
MCP_CONFIG="$HOME/.config/claude-code/mcp.json"
if [ -f "$MCP_CONFIG" ]; then
echo ""
echo "Updating MCP config to use installed path..."
# Check if config references the old path
if grep -q "target/release/blue" "$MCP_CONFIG" 2>/dev/null; then
if command -v jq &> /dev/null; then
jq '.mcpServers.blue.command = "blue"' "$MCP_CONFIG" > "$MCP_CONFIG.tmp" && mv "$MCP_CONFIG.tmp" "$MCP_CONFIG"
echo -e "${GREEN}MCP config updated${NC}"
else
echo "Install jq to auto-update MCP config, or manually change:"
echo " command: \"blue\""
fi
fi
fi
# Install Blue skills to Claude Code (symlink, not copy)
SKILLS_DIR="$HOME/.claude/skills"
BLUE_SKILLS_DIR="$(cd "$(dirname "$0")" && pwd)/skills"
if [ -d "$BLUE_SKILLS_DIR" ] && [ -d "$HOME/.claude" ]; then
echo ""
echo "Installing Blue skills..."
mkdir -p "$SKILLS_DIR"
for skill in "$BLUE_SKILLS_DIR"/*; do
if [ -d "$skill" ]; then
skill_name=$(basename "$skill")
target="$SKILLS_DIR/$skill_name"
# Remove existing symlink, file, or directory
rm -rf "$target" 2>/dev/null
ln -s "$skill" "$target"
echo " Linked skill: $skill_name -> $skill"
fi
done
echo -e "${GREEN}Skills linked to $SKILLS_DIR${NC}"
fi
# Install Blue hooks to Claude Code (RFC 0041: write to settings.json, not hooks.json)
SETTINGS_FILE="$HOME/.claude/settings.json"
HOOKS_FILE="$HOME/.claude/hooks.json"
BLUE_ROOT="$(cd "$(dirname "$0")" && pwd)"
if [ -d "$HOME/.claude" ]; then
echo ""
echo "Configuring Blue hooks..."
# Migrate hooks.json to settings.json if both exist (RFC 0041)
if [ -f "$HOOKS_FILE" ] && [ -f "$SETTINGS_FILE" ]; then
echo " Migrating hooks.json to settings.json..."
if command -v jq &> /dev/null; then
jq -s '.[0] * .[1]' "$SETTINGS_FILE" "$HOOKS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
mv "$HOOKS_FILE" "$HOOKS_FILE.migrated"
echo -e " ${GREEN}Migration complete (old file: hooks.json.migrated)${NC}"
else
echo -e " ${RED}Install jq to migrate hooks.json${NC}"
fi
fi
# Ensure settings.json exists with hooks structure
if [ ! -f "$SETTINGS_FILE" ]; then
echo '{"hooks":{}}' > "$SETTINGS_FILE"
fi
# Update hooks in settings.json using jq if available
if command -v jq &> /dev/null; then
jq --arg blue_root "$BLUE_ROOT" '
.hooks.SessionStart = [
{
"matcher": "",
"hooks": [{"type": "command", "command": ($blue_root + "/hooks/session-start")}]
},
{
"matcher": "compact",
"hooks": [{"type": "command", "command": ($blue_root + "/hooks/context-restore")}]
}
] |
.hooks.PreCompact = [
{
"matcher": "",
"hooks": [{"type": "command", "command": ($blue_root + "/hooks/pre-compact")}]
}
] |
.hooks.PreToolUse = [
{
"matcher": "blue_*",
"hooks": [{"type": "command", "command": "blue session-heartbeat"}]
}
] |
.hooks.SessionEnd = [
{
"matcher": "",
"hooks": [{"type": "command", "command": "blue session-end"}]
}
]
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
echo -e "${GREEN}Hooks configured in settings.json${NC}"
else
echo -e "${RED}jq is required for hook configuration${NC}"
echo "Install jq: brew install jq (macOS) or apt install jq (Linux)"
fi
fi
echo ""
echo "Done. Restart Claude Code to use the new installation."