ee6abbef37
Signed-off-by: cyphercodes <cyphercodes@users.noreply.github.com>
379 lines
13 KiB
Bash
Executable File
379 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# install.sh — One-line installer for codebase-memory-mcp.
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
|
|
# curl -fsSL ... | bash -s -- --dir /path # Custom install directory
|
|
#
|
|
# Environment:
|
|
# CBM_DOWNLOAD_URL Override base URL for downloads (for testing)
|
|
|
|
# Wrap in main() to prevent partial execution from piped downloads.
|
|
# If curl|bash is interrupted mid-transfer, bash would execute the partial
|
|
# script. With this wrapper, the function is defined but main() is never
|
|
# called because the final line hasn't arrived yet.
|
|
main() {
|
|
|
|
REPO="DeusData/codebase-memory-mcp"
|
|
INSTALL_DIR="$HOME/.local/bin"
|
|
SKIP_CONFIG=false
|
|
CBM_DOWNLOAD_URL="${CBM_DOWNLOAD_URL:-https://github.com/${REPO}/releases/latest/download}"
|
|
|
|
# Security: every remote hop must remain HTTPS. Plain HTTP is accepted only
|
|
# for an exact loopback authority used by local smoke tests, with redirects
|
|
# disabled so a local fixture cannot bounce the installer to the network.
|
|
is_loopback_http_url() {
|
|
[[ "$1" =~ ^http://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?([/?\#].*)?$ ]]
|
|
}
|
|
|
|
if [[ "$CBM_DOWNLOAD_URL" == https://* ]]; then
|
|
CBM_DOWNLOAD_LOOPBACK=false
|
|
elif is_loopback_http_url "$CBM_DOWNLOAD_URL"; then
|
|
CBM_DOWNLOAD_LOOPBACK=true
|
|
else
|
|
echo "error: refusing non-HTTPS download URL: $CBM_DOWNLOAD_URL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
download_file() {
|
|
local url="$1"
|
|
local destination="$2"
|
|
local progress="$3"
|
|
if [ "$CBM_DOWNLOAD_LOOPBACK" = true ]; then
|
|
is_loopback_http_url "$url" || {
|
|
echo "error: loopback download escaped its authority: $url" >&2
|
|
return 1
|
|
}
|
|
if command -v curl &>/dev/null; then
|
|
local curl_args=(-fS --noproxy '*' --proto '=http')
|
|
[ "$progress" = true ] && curl_args+=(--progress-bar) || curl_args+=(-s)
|
|
curl "${curl_args[@]}" -o "$destination" "$url"
|
|
elif command -v wget &>/dev/null; then
|
|
local wget_args=(--no-proxy --max-redirect=0)
|
|
[ "$progress" = true ] && wget_args+=(--show-progress) || wget_args+=(-q)
|
|
wget "${wget_args[@]}" -O "$destination" "$url"
|
|
else
|
|
echo "error: curl or wget required" >&2
|
|
return 1
|
|
fi
|
|
return
|
|
fi
|
|
|
|
[[ "$url" == https://* ]] || {
|
|
echo "error: HTTPS download downgraded: $url" >&2
|
|
return 1
|
|
}
|
|
if command -v curl &>/dev/null; then
|
|
local curl_args=(-fSL --max-redirs 5 --proto '=https' --proto-redir '=https')
|
|
[ "$progress" = true ] && curl_args+=(--progress-bar) || curl_args+=(-sS)
|
|
curl "${curl_args[@]}" -o "$destination" "$url"
|
|
elif command -v wget &>/dev/null; then
|
|
local wget_args=(--https-only --max-redirect=5)
|
|
[ "$progress" = true ] && wget_args+=(--show-progress) || wget_args+=(-q)
|
|
wget "${wget_args[@]}" -O "$destination" "$url"
|
|
else
|
|
echo "error: curl or wget required" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dir=*) INSTALL_DIR="${arg#--dir=}" ;;
|
|
--skip-config) SKIP_CONFIG=true ;;
|
|
--help|-h)
|
|
echo "Usage: install.sh [--dir=<path>] [--skip-config]"
|
|
echo " --dir PATH Install directory (default: ~/.local/bin)"
|
|
echo " --skip-config Skip automatic agent configuration"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
# Handle --dir <path> (space-separated)
|
|
prev=""
|
|
for arg in "$@"; do
|
|
if [ "$prev" = "--dir" ]; then
|
|
INSTALL_DIR="$arg"
|
|
fi
|
|
prev="$arg"
|
|
done
|
|
|
|
detect_os() {
|
|
case "$(uname -s)" in
|
|
Darwin) echo "darwin" ;;
|
|
Linux) echo "linux" ;;
|
|
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
|
|
*) echo "error: unsupported OS: $(uname -s)" >&2; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
arm64|aarch64) echo "arm64" ;;
|
|
x86_64|amd64)
|
|
# Rosetta detection: shell reports x86_64 but hardware is Apple Silicon
|
|
if [ "$(uname -s)" = "Darwin" ] && sysctl -n machdep.cpu.brand_string 2>/dev/null | grep -qi apple; then
|
|
echo "arm64"
|
|
else
|
|
echo "amd64"
|
|
fi
|
|
;;
|
|
*) echo "error: unsupported architecture: $arch" >&2; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
OS=$(detect_os)
|
|
ARCH=$(detect_arch)
|
|
|
|
echo "codebase-memory-mcp installer"
|
|
echo " os: $OS"
|
|
echo " arch: $ARCH"
|
|
echo " target: $INSTALL_DIR/codebase-memory-mcp"
|
|
echo ""
|
|
|
|
# Build download URL
|
|
if [ "$OS" = "windows" ]; then
|
|
EXT="zip"
|
|
else
|
|
EXT="tar.gz"
|
|
fi
|
|
|
|
# Linux ships a fully-static "-portable" build; the standard linux binary
|
|
# dynamically links glibc 2.38+ and fails on older distros (Debian 11, RHEL 8,
|
|
# Ubuntu 20.04). macOS/Windows have no such variant.
|
|
PORTABLE=""
|
|
[ "$OS" = "linux" ] && PORTABLE="-portable"
|
|
|
|
ARCHIVE="codebase-memory-mcp-${OS}-${ARCH}${PORTABLE}.${EXT}"
|
|
|
|
URL="${CBM_DOWNLOAD_URL}/${ARCHIVE}"
|
|
|
|
# Download
|
|
DLDIR=$(mktemp -d)
|
|
trap 'rm -rf "$DLDIR"' EXIT
|
|
|
|
echo "Downloading ${ARCHIVE}..."
|
|
download_file "$URL" "$DLDIR/$ARCHIVE" true
|
|
|
|
# Checksum verification is mandatory. Activation must never stop running CBM
|
|
# sessions for a candidate whose published digest was not positively verified.
|
|
CHECKSUM_URL="${CBM_DOWNLOAD_URL}/checksums.txt"
|
|
download_file "$CHECKSUM_URL" "$DLDIR/checksums.txt" false || {
|
|
echo "error: could not download checksums.txt" >&2
|
|
exit 1
|
|
}
|
|
CHECKSUM_BYTES=$(wc -c < "$DLDIR/checksums.txt" | tr -d '[:space:]')
|
|
case "$CHECKSUM_BYTES" in
|
|
''|*[!0-9]*)
|
|
echo "error: could not determine checksums.txt size" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ "$CHECKSUM_BYTES" -gt 1048576 ]; then
|
|
echo "error: checksums.txt exceeds the 1 MiB safety limit" >&2
|
|
exit 1
|
|
fi
|
|
awk -v archive="$ARCHIVE" \
|
|
'$2 == archive || $2 == "*" archive { print $1 }' \
|
|
"$DLDIR/checksums.txt" > "$DLDIR/matching-checksums.txt"
|
|
EXPECTED=""
|
|
while IFS= read -r digest; do
|
|
case "$digest" in
|
|
''|*[!0-9A-Fa-f]*)
|
|
echo "error: invalid SHA-256 digest for $ARCHIVE" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ "${#digest}" -ne 64 ]; then
|
|
echo "error: invalid SHA-256 digest length for $ARCHIVE" >&2
|
|
exit 1
|
|
fi
|
|
digest=$(printf '%s' "$digest" | tr 'A-F' 'a-f')
|
|
if [ -n "$EXPECTED" ] && [ "$EXPECTED" != "$digest" ]; then
|
|
echo "error: conflicting SHA-256 digests for $ARCHIVE" >&2
|
|
exit 1
|
|
fi
|
|
EXPECTED="$digest"
|
|
done < "$DLDIR/matching-checksums.txt"
|
|
if [ -z "$EXPECTED" ]; then
|
|
echo "error: no SHA-256 digest for $ARCHIVE in checksums.txt" >&2
|
|
exit 1
|
|
fi
|
|
if command -v sha256sum &>/dev/null; then
|
|
ACTUAL=$(sha256sum "$DLDIR/$ARCHIVE" | awk '{print $1}')
|
|
elif command -v shasum &>/dev/null; then
|
|
ACTUAL=$(shasum -a 256 "$DLDIR/$ARCHIVE" | awk '{print $1}')
|
|
else
|
|
echo "error: sha256sum or shasum is required to verify the download" >&2
|
|
exit 1
|
|
fi
|
|
ACTUAL=$(printf '%s' "$ACTUAL" | tr 'A-F' 'a-f')
|
|
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
|
echo "error: CHECKSUM MISMATCH — download may be corrupted!" >&2
|
|
echo " expected: $EXPECTED" >&2
|
|
echo " actual: $ACTUAL" >&2
|
|
exit 1
|
|
fi
|
|
echo "Checksum verified."
|
|
|
|
# Validate the complete archive namespace before extraction. Current and legacy
|
|
# release assets use the same four-member root layout; anything outside that
|
|
# closed set is a release-integrity failure, not a sidecar to ignore.
|
|
if [ "$OS" = "windows" ]; then
|
|
ARCHIVE_BINARY="codebase-memory-mcp.exe"
|
|
ARCHIVE_INSTALLER="install.ps1"
|
|
else
|
|
ARCHIVE_BINARY="codebase-memory-mcp"
|
|
ARCHIVE_INSTALLER="install.sh"
|
|
fi
|
|
ARCHIVE_MEMBERS_FILE="$DLDIR/archive-members.txt"
|
|
if [ "$EXT" = "zip" ]; then
|
|
if ! unzip -Z1 "$DLDIR/$ARCHIVE" > "$ARCHIVE_MEMBERS_FILE"; then
|
|
echo "error: could not enumerate release archive" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
if ! tar -tzf "$DLDIR/$ARCHIVE" > "$ARCHIVE_MEMBERS_FILE"; then
|
|
echo "error: could not enumerate release archive" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
BINARY_MEMBERS=0
|
|
LICENSE_MEMBERS=0
|
|
INSTALLER_MEMBERS=0
|
|
NOTICE_MEMBERS=0
|
|
ARCHIVE_MEMBER_COUNT=0
|
|
while IFS= read -r member || [ -n "$member" ]; do
|
|
ARCHIVE_MEMBER_COUNT=$((ARCHIVE_MEMBER_COUNT + 1))
|
|
case "$member" in
|
|
"$ARCHIVE_BINARY") BINARY_MEMBERS=$((BINARY_MEMBERS + 1)) ;;
|
|
LICENSE) LICENSE_MEMBERS=$((LICENSE_MEMBERS + 1)) ;;
|
|
"$ARCHIVE_INSTALLER") INSTALLER_MEMBERS=$((INSTALLER_MEMBERS + 1)) ;;
|
|
THIRD_PARTY_NOTICES.md) NOTICE_MEMBERS=$((NOTICE_MEMBERS + 1)) ;;
|
|
*)
|
|
echo "error: release archive contains unexpected member: $member" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done < "$ARCHIVE_MEMBERS_FILE"
|
|
|
|
if [ "$BINARY_MEMBERS" -ne 1 ] || [ "$LICENSE_MEMBERS" -ne 1 ] ||
|
|
[ "$INSTALLER_MEMBERS" -ne 1 ] || [ "$NOTICE_MEMBERS" -ne 1 ] ||
|
|
[ "$ARCHIVE_MEMBER_COUNT" -ne 4 ]; then
|
|
echo "error: release archive does not match the exact member set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Extract
|
|
echo "Extracting..."
|
|
if [ "$EXT" = "zip" ]; then
|
|
unzip -q "$DLDIR/$ARCHIVE" -d "$DLDIR"
|
|
else
|
|
tar --no-same-owner -xzf "$DLDIR/$ARCHIVE" -C "$DLDIR"
|
|
fi
|
|
|
|
for extracted_member in "$ARCHIVE_BINARY" LICENSE "$ARCHIVE_INSTALLER" \
|
|
THIRD_PARTY_NOTICES.md; do
|
|
if [ ! -f "$DLDIR/$extracted_member" ] || [ -L "$DLDIR/$extracted_member" ]; then
|
|
echo "error: release member is not a regular file: $extracted_member" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
DLBIN="$DLDIR/$ARCHIVE_BINARY"
|
|
if [ ! -f "$DLBIN" ] || [ -L "$DLBIN" ]; then
|
|
echo "error: binary not found after extraction" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# macOS: fix signing
|
|
if [ "$OS" = "darwin" ]; then
|
|
echo "Fixing macOS code signing..."
|
|
# A curl-downloaded archive usually carries no quarantine attribute at all,
|
|
# and xattr then prints "No such xattr: com.apple.quarantine" on stderr.
|
|
# That harmless line was read as the cause of an unrelated install failure
|
|
# and became a bug report's title (#1537) — silence it; nothing here is an
|
|
# error worth showing.
|
|
xattr -d com.apple.quarantine "$DLBIN" >/dev/null 2>&1 || true
|
|
codesign --sign - --force "$DLBIN" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Verify the candidate before it requests account-wide maintenance. The
|
|
# candidate itself owns process draining and the transactional target swap.
|
|
chmod 755 "$DLBIN"
|
|
if ! CANDIDATE_VERSION=$("$DLBIN" --version 2>&1); then
|
|
echo "error: downloaded binary failed to run" >&2
|
|
exit 1
|
|
fi
|
|
echo "Verified candidate: $CANDIDATE_VERSION"
|
|
|
|
DEST="$INSTALL_DIR/codebase-memory-mcp"
|
|
INSTALL_ARGS=(-y --force "--dir=$INSTALL_DIR")
|
|
if [ "$SKIP_CONFIG" = true ]; then
|
|
INSTALL_ARGS+=(--skip-config)
|
|
fi
|
|
"$DLBIN" install "${INSTALL_ARGS[@]}"
|
|
|
|
# Place the installer beside the binary so `update` can point at a local file
|
|
# instead of a URL, and so the next update uses THIS release's installer.
|
|
#
|
|
# Two things make this safe. The source is the copy from the archive we just
|
|
# checksum-verified -- never "$0", which does not exist under `curl | bash` and
|
|
# would pin us to the OLD installer forever. And it is published by atomic
|
|
# rename, never by writing over the live path: bash reads a script incrementally
|
|
# by byte offset, so overwriting the file it is executing continues reading the
|
|
# NEW bytes at the OLD offset. That fails silently and bizarrely, which is worse
|
|
# than failing loudly.
|
|
#
|
|
# Best effort by design: a user who cannot write to INSTALL_DIR still gets a
|
|
# working install, and `update` falls back to explaining where to find it.
|
|
DL_INSTALLER="$DLDIR/install.sh"
|
|
if [ -f "$DL_INSTALLER" ]; then
|
|
INSTALLER_TMP="$INSTALL_DIR/.install.sh.$$"
|
|
if cp "$DL_INSTALLER" "$INSTALLER_TMP" 2>/dev/null &&
|
|
chmod 755 "$INSTALLER_TMP" 2>/dev/null &&
|
|
mv -f "$INSTALLER_TMP" "$INSTALL_DIR/install.sh" 2>/dev/null; then
|
|
echo "Installed updater -> $INSTALL_DIR/install.sh"
|
|
else
|
|
rm -f "$INSTALLER_TMP" 2>/dev/null || true
|
|
echo "note: could not place install.sh in $INSTALL_DIR (update will explain where to find it)"
|
|
fi
|
|
fi
|
|
|
|
# Verify
|
|
VERSION=$("$DEST" --version 2>&1) || {
|
|
echo "error: installed binary failed to run" >&2
|
|
if [ "$OS" = "darwin" ]; then
|
|
echo " try: xattr -cr $DEST && codesign --force --sign - $DEST" >&2
|
|
fi
|
|
exit 1
|
|
}
|
|
echo "Installed: $VERSION"
|
|
|
|
# Agent configuration is part of the candidate-owned activation window.
|
|
if [ "$SKIP_CONFIG" = true ]; then
|
|
echo ""
|
|
echo "Skipping agent configuration (--skip-config)"
|
|
fi
|
|
|
|
# PATH check
|
|
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
|
echo ""
|
|
echo "NOTE: $INSTALL_DIR is not in your PATH."
|
|
echo "Add it to your shell config:"
|
|
echo ""
|
|
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Done! Restart your coding agent to start using codebase-memory-mcp."
|
|
|
|
} # end main()
|
|
|
|
main "$@"
|