#!/usr/bin/env bash set -euo pipefail program_name="colimit-install.sh" default_release_base_url="https://install.colimit.ai/artifacts" default_colimit_home="${XDG_DATA_HOME:-$HOME/.local/share}/colimit" release_base_url="${COLIMIT_RELEASE_BASE_URL:-${COLIMIT_INSTALL_BASE_URL:-$default_release_base_url}}" colimit_home="${COLIMIT_HOME:-$default_colimit_home}" colimit_bin_dir="${COLIMIT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}" cleanup_tmp_dir="" download_cmd="" checksum_cmd="" log() { printf '%s\n' "$*" } cleanup() { if [ -n "${cleanup_tmp_dir:-}" ]; then rm -rf "$cleanup_tmp_dir" fi } fail() { printf '%s: %s\n' "$program_name" "$*" >&2 exit 1 } has_tty() { { : /dev/tty; } 2>/dev/null } prompt_yes_default() { local prompt="$1" local answer printf '%s [Y/n] ' "$prompt" > /dev/tty IFS= read -r answer < /dev/tty || answer="" case "$answer" in ""|y|Y|yes|YES|Yes) return 0 ;; n|N|no|NO|No) return 1 ;; *) fail "expected y or n" ;; esac } usage() { cat <<'EOF' Usage: colimit-install.sh Installs the latest published Colimit bundle. Options: -h, --help Show this help. Environment: COLIMIT_HOME Install root. Defaults to ${XDG_DATA_HOME:-$HOME/.local/share}/colimit. COLIMIT_BIN_DIR Command directory. Defaults to ${XDG_BIN_HOME:-$HOME/.local/bin}. COLIMIT_RELEASE_BASE_URL Release asset base URL. Defaults to https://install.colimit.ai/artifacts. COLIMIT_INSTALL_YES=1 Install without prompting. EOF } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in -h|--help) usage exit 0 ;; --) shift [ "$#" -eq 0 ] || fail "unexpected positional arguments: $*" ;; -*) fail "unknown option: $1" ;; *) fail "unexpected positional argument: $1" ;; esac done } require_command() { command -v "$1" >/dev/null 2>&1 || fail "$1 is required" } require_tools() { local tool for tool in awk basename chmod dirname find grep ln mkdir mktemp mv rm sed tar tr uname; do require_command "$tool" done if command -v curl >/dev/null 2>&1; then download_cmd="curl" elif command -v wget >/dev/null 2>&1; then download_cmd="wget" else fail "curl or wget is required" fi if command -v sha256sum >/dev/null 2>&1; then checksum_cmd="sha256sum" elif command -v shasum >/dev/null 2>&1; then checksum_cmd="shasum" else fail "sha256sum or shasum is required" fi } detect_target() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$arch" in x86_64|amd64) arch="x86_64" ;; arm64|aarch64) arch="aarch64" ;; *) fail "unsupported CPU architecture: $arch" ;; esac case "$os" in Linux) printf '%s-unknown-linux-gnu\n' "$arch" ;; Darwin) printf '%s-apple-darwin\n' "$arch" ;; *) fail "unsupported operating system: $os" ;; esac } require_download_url() { case "$1" in https://*) ;; *) if [ "${COLIMIT_ALLOW_INSECURE:-}" != "1" ]; then fail "refusing non-HTTPS download URL: $1" fi ;; esac } download_to() { local url="$1" local output="$2" local label="$3" require_download_url "$url" log "Downloading $label:" log " $url" if [ "$download_cmd" = "curl" ]; then case "$url" in https://*) curl --proto '=https' --tlsv1.2 -fL --progress-bar "$url" -o "$output" ;; *) curl -fL --progress-bar "$url" -o "$output" ;; esac else wget -O "$output" "$url" fi } confirm_install() { local target="$1" local asset_url="$2" local checksum_url="$3" local action="install" if [ -e "$colimit_home/current" ] || [ -L "$colimit_home/current" ]; then action="update" fi cat </dev/null 2>&1; then return 0 fi if [ "${COLIMIT_INSTALL_YES:-}" = "1" ]; then log "" log "claude-code-acp is not installed; COLIMIT_INSTALL_YES=1, installing without prompt." install_claude_code_acp_with_npm return 0 fi if ! has_tty; then log "" log "claude-code-acp is not installed." log "Run this when you want Claude-backed synthesis:" log " npm install -g @zed-industries/claude-code-acp" return 0 fi log "" if prompt_yes_default "Install claude-code-acp?"; then install_claude_code_acp_with_npm else log "Skipping claude-code-acp install." fi } install_claude_code_acp_with_npm() { if ! command -v npm >/dev/null 2>&1; then fail "npm is required to install claude-code-acp; install Node.js/npm, then rerun this installer" fi npm install -g @zed-industries/claude-code-acp } hash_file() { local file="$1" if [ "$checksum_cmd" = "sha256sum" ]; then sha256sum "$file" | awk '{print $1}' else shasum -a 256 "$file" | awk '{print $1}' fi } parse_checksum() { local checksum_file="$1" local checksum checksum="$(awk 'NF { print $1; exit }' "$checksum_file")" checksum="${checksum#sha256:}" checksum="$(printf '%s' "$checksum" | tr 'A-F' 'a-f')" printf '%s' "$checksum" | grep -Eq '^[0-9a-f]{64}$' || fail "invalid checksum file" printf '%s\n' "$checksum" } find_payload_root() { local staging="$1" local root="" local count=0 local candidate while IFS= read -r candidate; do root="$candidate" count=$((count + 1)) done < <(find "$staging" -mindepth 1 -maxdepth 1 -type d -print) [ "$count" -eq 1 ] || fail "archive must contain exactly one top-level directory" printf '%s\n' "$root" } validate_payload() { local root="$1" local target="$2" local templates="$root/share/colimit/templates" local colimit_code_data="$root/share/colimit/colimit-code" local root_name [ -x "$root/bin/colimit-code" ] || fail "archive is missing executable bin/colimit-code" [ -x "$root/bin/colimit-spec" ] || fail "archive is missing executable bin/colimit-spec" [ -d "$templates" ] || fail "archive is missing share/colimit/templates" find "$templates" -type f -print -quit | grep -q . || fail "archive template directory is empty" [ -f "$colimit_code_data/themes.json" ] || fail "archive is missing share/colimit/colimit-code/themes.json" root_name="$(basename "$root")" [ "$root_name" = "colimit-$target" ] || fail "archive root '$root_name' does not match target '$target'" } install_current() { local source_root="$1" local target="$colimit_home/current" local parent tmp_target old_target parent="$(dirname "$target")" tmp_target="$parent/.install.$$" old_target="$parent/.old.$$" mkdir -p "$parent" rm -rf "$tmp_target" "$old_target" mv "$source_root" "$tmp_target" if [ -e "$target" ] || [ -L "$target" ]; then mv "$target" "$old_target" mv "$tmp_target" "$target" rm -rf "$old_target" else mv "$tmp_target" "$target" fi } link_command() { local name="$1" local source="$colimit_home/current/bin/$name" local dest="$colimit_bin_dir/$name" if [ -e "$dest" ] && [ ! -L "$dest" ]; then fail "$dest exists and is not a symlink" fi rm -f "$dest" ln -s "$source" "$dest" } is_generated_wrapper() { local file="$1" grep -Eq '^# Generated by colimit-install(\.sh)?\.$' "$file" 2>/dev/null } shell_quote() { printf "'" printf '%s' "$1" | sed "s/'/'\\\\''/g" printf "'" } write_colimit_code_wrapper() { local dest="$colimit_bin_dir/colimit-code" local tmp="$colimit_bin_dir/.colimit-code.$$" local real_binary="$colimit_home/current/bin/colimit-code" local template_dir="$colimit_home/current/share/colimit/templates" local colimit_code_data="$colimit_home/current/share/colimit/colimit-code" if [ -e "$dest" ] && [ ! -L "$dest" ] && ! is_generated_wrapper "$dest"; then fail "$dest exists and was not generated by colimit-install.sh" fi { printf '#!/usr/bin/env sh\n' printf '# Generated by colimit-install.sh.\n' printf 'export COLIMIT_TEMPLATE_DIR=%s\n' "$(shell_quote "$template_dir")" printf 'export colimit_code_datadir=%s\n' "$(shell_quote "$colimit_code_data")" printf 'exec %s "$@"\n' "$(shell_quote "$real_binary")" } > "$tmp" chmod +x "$tmp" rm -f "$dest" mv "$tmp" "$dest" } write_env_file() { local env_file="$colimit_home/env" local tmp_env="$colimit_home/.env.$$" local template_dir="$colimit_home/current/share/colimit/templates" local colimit_code_data="$colimit_home/current/share/colimit/colimit-code" { printf '# Generated by the Colimit installer.\n' printf 'export PATH=%s:"%s"\n' "$(shell_quote "$colimit_bin_dir")" "\$PATH" printf 'export COLIMIT_TEMPLATE_DIR=%s\n' "$(shell_quote "$template_dir")" printf 'export colimit_code_datadir=%s\n' "$(shell_quote "$colimit_code_data")" } > "$tmp_env" mv -f "$tmp_env" "$env_file" } path_contains_bin_dir() { case ":$PATH:" in *":$colimit_bin_dir:"*) return 0 ;; *) return 1 ;; esac } main() { local target asset asset_url checksum_url tmp_dir checksum_file archive_file staging payload_root actual_sha expected_sha parse_args "$@" require_tools target="$(detect_target)" asset="colimit-${target}.tar.gz" asset_url="${release_base_url%/}/$asset" checksum_url="$asset_url.sha256" confirm_install "$target" "$asset_url" "$checksum_url" tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/colimit-install.XXXXXX")" cleanup_tmp_dir="$tmp_dir" trap cleanup EXIT HUP INT TERM archive_file="$tmp_dir/$asset" checksum_file="$tmp_dir/$asset.sha256" staging="$tmp_dir/staging" download_to "$checksum_url" "$checksum_file" "checksum" download_to "$asset_url" "$archive_file" "bundle" log "Verifying checksum..." expected_sha="$(parse_checksum "$checksum_file")" actual_sha="$(hash_file "$archive_file")" [ "$actual_sha" = "$expected_sha" ] || fail "checksum mismatch for $asset_url" mkdir -p "$staging" log "Extracting bundle..." tar -xzf "$archive_file" -C "$staging" log "Validating bundle..." payload_root="$(find_payload_root "$staging")" validate_payload "$payload_root" "$target" log "Installing bundle..." install_current "$payload_root" mkdir -p "$colimit_bin_dir" write_colimit_code_wrapper link_command colimit-spec write_env_file install_claude_code_acp log "Installed latest Colimit bundle for $target" log " colimit-code -> $colimit_bin_dir/colimit-code" log " colimit-spec -> $colimit_bin_dir/colimit-spec" log " templates -> $colimit_home/current/share/colimit/templates" log " themes -> $colimit_home/current/share/colimit/colimit-code/themes.json" if ! path_contains_bin_dir; then log "" log "Add Colimit to your shell environment with:" log " . \"$colimit_home/env\"" fi } main "$@"