#!/bin/sh set -eu GITHUB_REPO="codag-megalith/codag-cli" BINARY="codag" INSTALL_DIR="${CODAG_INSTALL_DIR:-}" if [ -z "$INSTALL_DIR" ]; then if [ -z "${HOME:-}" ]; then printf 'Error: HOME is not set. Set CODAG_INSTALL_DIR to choose an install directory.\n' >&2 exit 1 fi INSTALL_DIR="${HOME}/.local/bin" fi if [ -t 1 ]; then RED="$(printf '\033[0;31m')" GREEN="$(printf '\033[0;32m')" YELLOW="$(printf '\033[0;33m')" BLUE="$(printf '\033[0;34m')" BOLD="$(printf '\033[1m')" NC="$(printf '\033[0m')" else RED="" GREEN="" YELLOW="" BLUE="" BOLD="" NC="" fi info() { printf '%b%s%b\n' "${BLUE}==>${NC} ${BOLD}" "$1" "${NC}" } success() { printf '%b%s%b\n' "${GREEN}==>${NC} ${BOLD}" "$1" "${NC}" } warn() { printf '%b %s\n' "${YELLOW}Warning:${NC}" "$1" } error() { printf '%b %s\n' "${RED}Error:${NC}" "$1" >&2 exit 1 } detect_os() { os="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$os" in darwin) echo "darwin" ;; linux) echo "linux" ;; *) error "Unsupported operating system: $os" ;; esac } detect_arch() { arch="$(uname -m)" case "$arch" in x86_64|amd64) echo "amd64" ;; arm64|aarch64) echo "arm64" ;; *) error "Unsupported architecture: $arch" ;; esac } github_api() { url="$1" if [ -n "${GITHUB_TOKEN:-}" ]; then curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$url" else curl -fsSL "$url" fi } get_latest_version() { url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" version="$(github_api "$url" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"tag_name": *"v?([^"]+)".*/\1/')" if [ -z "$version" ]; then error "Failed to fetch latest version from GitHub. Please check your internet connection." fi echo "$version" } download_file() { url="$1" output="$2" if ! curl -fsSL "$url" -o "$output"; then error "Failed to download: ${url}" fi } verify_checksum() { file="$1" expected="$2" if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$file" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$file" | awk '{print $1}')" else warn "No checksum tool found (sha256sum or shasum). Skipping verification." return 0 fi if [ "$actual" != "$expected" ]; then error "Checksum verification failed! Expected: ${expected} Actual: ${actual}" fi } extract_archive() { archive_path="$1" tmp_dir="$2" tar -xzf "$archive_path" -C "$tmp_dir" } install_bash_completion() { binary_path="$1" if [ -z "${HOME:-}" ]; then return 0 fi completion_dir="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions" completion_file="${completion_dir}/${BINARY}" if ! "$binary_path" completion bash >/dev/null 2>&1; then warn "Bash completion generator is not available in this binary." return 0 fi if mkdir -p "$completion_dir" 2>/dev/null && "$binary_path" completion bash > "$completion_file" 2>/dev/null; then success "Bash completion installed to ${completion_file}" printf ' Restart bash, or run: %bsource %s%b\n' "$BOLD" "$completion_file" "$NC" else warn "Could not install bash completion. To enable it manually, run:" printf ' %b%s completion bash > %s%b\n' "$BOLD" "$binary_path" "$completion_file" "$NC" printf ' %bsource %s%b\n' "$BOLD" "$completion_file" "$NC" fi } escape_double_quoted() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g; s/`/\\`/g' } shell_startup_target() { if [ -z "${HOME:-}" ]; then return 1 fi shell_name="$(basename "${SHELL:-sh}")" case "$shell_name" in zsh) printf '%s|sh\n' "$HOME/.zshrc" ;; bash) if [ -f "$HOME/.bash_profile" ]; then printf '%s|sh\n' "$HOME/.bash_profile" else printf '%s|sh\n' "$HOME/.bashrc" fi ;; fish) printf '%s|fish\n' "$HOME/.config/fish/config.fish" ;; *) printf '%s|sh\n' "$HOME/.profile" ;; esac } ensure_install_dir_on_path() { install_dir="$1" target="$(shell_startup_target || true)" if [ -z "$target" ]; then warn "Could not determine a shell startup file. Add ${install_dir} to PATH manually." return 1 fi profile="${target%%|*}" syntax="${target##*|}" escaped_dir="$(escape_double_quoted "$install_dir")" tmp_profile="${profile}.codag.tmp.$$" if ! mkdir -p "$(dirname "$profile")" 2>/dev/null; then warn "Could not create $(dirname "$profile"). Add ${install_dir} to PATH manually." return 1 fi if [ -f "$profile" ]; then awk ' $0 == "# >>> codag PATH >>>" { skip = 1; next } $0 == "# <<< codag PATH <<<" { skip = 0; next } skip != 1 { print } ' "$profile" > "$tmp_profile" || { rm -f "$tmp_profile" warn "Could not update ${profile}. Add ${install_dir} to PATH manually." return 1 } else : > "$tmp_profile" fi if [ -s "$tmp_profile" ]; then printf '\n' >> "$tmp_profile" fi printf '# >>> codag PATH >>>\n' >> "$tmp_profile" if [ "$syntax" = "fish" ]; then printf 'fish_add_path -g "%s"\n' "$escaped_dir" >> "$tmp_profile" else printf 'export PATH="%s:$PATH"\n' "$escaped_dir" >> "$tmp_profile" fi printf '# <<< codag PATH <<<\n' >> "$tmp_profile" if mv "$tmp_profile" "$profile"; then success "Added ${install_dir} to PATH in ${profile}" if [ "$syntax" = "fish" ]; then printf ' Restart your shell after setup, or run: %bfish_add_path -g "%s"%b\n' "$BOLD" "$install_dir" "$NC" else printf ' Restart your shell after setup, or run: %bexport PATH="%s:$PATH"%b\n' "$BOLD" "$install_dir" "$NC" fi return 0 fi rm -f "$tmp_profile" warn "Could not update ${profile}. Add ${install_dir} to PATH manually." return 1 } main() { if ! command -v curl >/dev/null 2>&1; then error "curl is required but not installed. Please install curl and try again." fi info "Installing Codag CLI..." os="$(detect_os)" arch="$(detect_arch)" info "Detected platform: ${os}/${arch}" info "Fetching latest version..." version="$(get_latest_version)" version="${version#v}" info "Installing version: ${version}" binary_file="${BINARY}" archive_name="codag_${os}_${arch}.tar.gz" download_url="https://github.com/${GITHUB_REPO}/releases/download/v${version}/${archive_name}" checksums_url="https://github.com/${GITHUB_REPO}/releases/download/v${version}/checksums.txt" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' 0 info "Downloading ${archive_name}..." archive_path="${tmp_dir}/${archive_name}" download_file "$download_url" "$archive_path" info "Verifying checksum..." checksums_path="${tmp_dir}/checksums.txt" download_file "$checksums_url" "$checksums_path" expected_checksum="$(awk -v name="$archive_name" 'tolower($2) == tolower(name) { print $1; exit }' "$checksums_path")" if [ -z "$expected_checksum" ]; then error "Checksum for ${archive_name} not found in checksums.txt" fi verify_checksum "$archive_path" "$expected_checksum" success "Checksum verified" info "Extracting..." extract_archive "$archive_path" "$tmp_dir" binary_path="${tmp_dir}/${binary_file}" if [ ! -f "$binary_path" ]; then error "Archive did not contain ${binary_file}" fi chmod +x "$binary_path" 2>/dev/null || true info "Installing to ${INSTALL_DIR}..." mkdir -p "$INSTALL_DIR" if [ ! -w "$INSTALL_DIR" ]; then error "Cannot write to ${INSTALL_DIR}. Please check permissions." fi installed_binary="${INSTALL_DIR}/${binary_file}" mv "$binary_path" "$installed_binary" if "$installed_binary" version >/dev/null 2>&1; then success "Codag CLI v${version} installed to ${installed_binary}" else error "Installation completed but the binary failed to execute." fi install_bash_completion "$installed_binary" path_binary="$(command -v "$BINARY" 2>/dev/null || command -v "$binary_file" 2>/dev/null || true)" if [ -n "$path_binary" ] && [ "$path_binary" != "$installed_binary" ]; then printf '\n' printf ' %bWARNING: PATH conflict detected%b\n\n' "$YELLOW" "$NC" printf ' Installed to: %s\n' "$installed_binary" printf ' But %s resolves to: %s\n\n' "$BINARY" "$path_binary" ensure_install_dir_on_path "$INSTALL_DIR" || true printf '\n' elif [ -z "$path_binary" ]; then printf '\n' ensure_install_dir_on_path "$INSTALL_DIR" || true printf '\n' fi run_binary="$BINARY" if [ "$path_binary" != "$installed_binary" ]; then run_binary="$installed_binary" fi printf ' Try it now, no account required:\n\n' printf ' %b%s wrap -- cat /path/to/app.log%b\n\n' "$BOLD" "$run_binary" "$NC" printf ' Wire Codag into your agents:\n\n' printf ' %b%s setup%b\n\n' "$BOLD" "$run_binary" "$NC" printf ' After setup:\n\n' printf ' %b%s wrap -- vercel logs --since=1h%b\n' "$BOLD" "$run_binary" "$NC" printf '\n' } main "$@"