From ed980e90bd93a0f451e7bb4182a34f65d109cde1 Mon Sep 17 00:00:00 2001 From: zhishu Date: Tue, 28 Jul 2026 07:51:10 +0000 Subject: [PATCH] feat(install): curl-able installer/upgrader for binary + Claude Code plugin Auto-discovers the latest release (redirect + API fallback), verifies sha256, installs to ~/.local/bin, wires the Claude Code plugin with curl|bash-safe stdin handling, and refreshes plugin assets on upgrade. Docs updated across README / INSTALL.zh-CN / INSTALLATION / RELEASING; releases attach a tag-pinned copy. --- .goreleaser.yaml | 6 + CLAUDE.md | 5 +- README.md | 35 +++- RELEASING.md | 12 ++ docs/INSTALL.zh-CN.md | 63 +++++- docs/INSTALLATION.md | 52 +++-- install.sh | 461 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 589 insertions(+), 45 deletions(-) create mode 100755 install.sh diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6c2483c6..54562c19 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -36,6 +36,12 @@ archives: checksum: name_template: "checksums.txt" +release: + extra_files: + # Attach the installer so each release carries a tag-pinned copy + # (the canonical, always-latest copy is raw.githubusercontent.com/.../main/install.sh). + - glob: ./install.sh + changelog: sort: asc filters: diff --git a/CLAUDE.md b/CLAUDE.md index 32ab9786..3b1b854c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,10 +16,11 @@ go test ./... # unit tests (all tests NOT tagged //go:build go test -tags e2e ./internal/server/... # e2e tests (separate CI job; required check) go test ./internal/store/ -run TestName # single package / single test make templ # regenerate templ → *_templ.go after editing dashboard .templ files -./setup.sh # link repo skills/* into .claude/skills, .codex/skills, .gemini/skills +./setup.sh # link repo skills/* into .claude/skills, .codex/skills, .gemini/skills (DEV-ONLY — not the user installer) +./install.sh # END-USER installer/upgrader: latest release binary + Claude Code plugin (curl-able from raw.githubusercontent.com) ``` -CI (`.github/workflows/ci.yml`) runs exactly `go test ./...` and the e2e job — no separate lint step. There is no Makefile build target; `make` only wraps templ generation. +CI (`.github/workflows/ci.yml`) runs `go test ./...`, the e2e job, and an `install-script` job (bash -n + shellcheck + live install/upgrade smoke of install.sh) — no separate Go lint step. There is no Makefile build target; `make` only wraps templ generation. ## Skills Are Mandatory diff --git a/README.md b/README.md index f49e7865..58189eb3 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Three things differ from upstream. If you have used upstream engram before, thes | Area | Upstream (`Gentleman-Programming`) | This fork (`relytcloud`) | | --- | --- | --- | | **Storage** | Local SQLite only | Local SQLite **+ optional per-project MemoryLake backend** (`engram memorylake …`) | -| **Install** | `brew install gentleman-programming/tap/engram` | **GitHub Release archives** (no Homebrew tap) — download or build from source | +| **Install** | `brew install gentleman-programming/tap/engram` | `curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh \| bash` (binary + plugin; no Homebrew tap) | | **Claude Code / Codex marketplace** | `claude plugin marketplace add Gentleman-Programming/engram` | `claude plugin marketplace add relytcloud/engram` — wired into `engram setup claude-code` | Details: [Install](#install) · [Connect Your Agent](#connect-your-agent) · [Storage Backends](#storage-backends). @@ -76,19 +76,48 @@ Full ownership map and flows → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) · ## Install +**One command** (macOS / Linux, amd64 / arm64) — installs the latest binary AND wires the Claude Code plugin: + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash +``` + +What it does: discovers the **latest release automatically**, verifies the sha256 checksum, installs to `~/.local/bin/engram`, then runs `engram setup claude-code` (plugin marketplace + plugin + MCP config). **Upgrading = re-run the same command** — it refreshes both the binary and the plugin assets. + +Options go after `bash -s --`: + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh \ + | bash -s -- --dir /usr/local/bin --no-plugin +``` + +| Flag | Meaning | +|---|---| +| `--version X.Y.Z` | pin a release instead of the latest | +| `--dir ` | install directory (default `~/.local/bin`) | +| `--no-plugin` | binary only, skip Claude Code wiring | +| `--force` | reinstall even if already at the target version | +| `--dry-run` | show what would happen, change nothing | +| `--protocol slim` | enable the compact memory protocol during setup | + +### Manual install + Engram ships prebuilt, statically-linked binaries on the **[Releases page](https://github.com/relytcloud/engram/releases)** for **macOS / Linux / Windows × amd64 / arm64**. No Homebrew, no Node, no Python, no Docker — **one binary, one SQLite file**. Pick your `os_arch` from the release assets (`uname -sm` tells you which): ```bash -VER=0.2.0 # latest tag on the Releases page +# resolve the latest tag automatically (or set VER=X.Y.Z from the Releases page) +VER=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ + https://github.com/relytcloud/engram/releases/latest | sed 's|.*/tag/v||') # macOS Apple Silicon shown; swap darwin_arm64 for darwin_amd64 / linux_amd64 / linux_arm64 curl -fsSL -o engram.tar.gz \ "https://github.com/relytcloud/engram/releases/download/v${VER}/engram_${VER}_darwin_arm64.tar.gz" tar -xzf engram.tar.gz engram +mkdir -p ~/.local/bin install -m 0755 engram ~/.local/bin/engram # make sure ~/.local/bin is on your PATH -engram version # → 0.2.0 +engram version ``` > macOS binaries are ad-hoc signed (not notarized). If Gatekeeper blocks the first run: diff --git a/RELEASING.md b/RELEASING.md index ae7e82ab..5efe9e6c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -78,11 +78,23 @@ GitHub Release archives. (Homebrew was intentionally dropped; do not re-add a ## Installing a released binary +Preferred — the installer script (auto-discovers the latest release, verifies +sha256, installs the binary AND wires the Claude Code plugin): + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash +# pin a specific release instead: +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash -s -- --version X.Y.Z +``` + +Manual (pinned-version) alternative: + ```bash # Example: linux/amd64 curl -fsSL -o engram.tar.gz \ https://github.com/relytcloud/engram/releases/download/vX.Y.Z/engram_X.Y.Z_linux_amd64.tar.gz tar -xzf engram.tar.gz engram +mkdir -p ~/.local/bin install -m 0755 engram ~/.local/bin/engram # ensure ~/.local/bin is on PATH engram --version ``` diff --git a/docs/INSTALL.zh-CN.md b/docs/INSTALL.zh-CN.md index 6470def0..837b2ce8 100644 --- a/docs/INSTALL.zh-CN.md +++ b/docs/INSTALL.zh-CN.md @@ -6,9 +6,40 @@ Engram 是给 AI 编码 agent 用的**持久记忆**服务:单个 Go 二进制, --- -## 一、安装二进制 +## 零、一键安装(推荐) -有两种方式:**下载预编译包(推荐)** 或 **从源码编译**。 +一条命令完成「二进制安装 + Claude Code 插件接入」,自动发现并使用**最新版本**: + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash +``` + +它会:自动解析最新 release 版本 → 下载对应平台包并**校验 sha256** → 安装到 `~/.local/bin/engram` → 运行 `engram setup claude-code`(注册插件市场 + 装插件 + 写 MCP 配置)→ 升级时自动刷新插件资产。**升级 = 重跑同一条命令**。 + +传参数用 `bash -s --`: + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh \ + | bash -s -- --dir /usr/local/bin --no-plugin +``` + +| 参数 | 含义 | +|---|---| +| `--version X.Y.Z` | 锁定指定版本(默认自动用最新) | +| `--dir <路径>` | 安装目录(默认 `~/.local/bin`) | +| `--no-plugin` | 只装二进制,跳过 Claude Code 插件 | +| `--force` | 版本相同也强制重装 | +| `--dry-run` | 只打印将做什么,不改任何东西 | +| `--protocol slim` | 安装时启用紧凑记忆协议 | + +> 脚本仅支持 macOS / Linux(amd64/arm64);Windows 请看下方手动步骤。 +> 非交互管道下,allowlist 授权提问会被自动跳过(脚本会提示如何补做)。 + +--- + +## 一、手动安装二进制 + +不想用一键脚本时,有两种方式:**下载预编译包** 或 **从源码编译**。 ### 方式 A:下载预编译 release 包(推荐) @@ -69,10 +100,15 @@ install -m 0755 engram ~/.local/bin/engram ```bash curl -fsSL -O "https://github.com/relytcloud/engram/releases/download/v${VER}/checksums.txt" -shasum -a 256 -c checksums.txt --ignore-missing # macOS -# sha256sum -c checksums.txt --ignore-missing # Linux +# 注意:macOS 的 BSD shasum 不支持 --ignore-missing,用 awk 精确比对(两平台通用): +PKG="engram_${VER}_darwin_arm64.tar.gz" # 换成你下载的包名 +expected=$(awk -v f="$PKG" '$2 == f {print $1}' checksums.txt) +actual=$(shasum -a 256 "$PKG" | awk '{print $1}') # Linux 用 sha256sum +[ "$expected" = "$actual" ] && echo OK || echo "校验失败,不要安装" ``` +> 一键脚本(上文「零」节)默认自动做这一步。 + ### 方式 B:从源码编译 需要 Go 1.25+。产物与 release 包一致(纯 Go,无需 CGO)。 @@ -126,7 +162,7 @@ engram setup claude-code 它会自动做三件事: 1. `claude plugin marketplace add relytcloud/engram`(注册插件市场,幂等); -2. `claude plugin install engram@engram`(安装插件:hooks + skills); +2. `claude plugin install engram`(安装插件:hooks + skills;代码当前用的是不带限定的插件名); 3. 写入用户级 MCP 配置 `~/.claude/mcp/engram.json`,内容形如: ```json @@ -169,17 +205,22 @@ rm -f ~/.claude/mcp/engram.json # 删 MCP 配置 ### 老用户升级(v0.4.0 重要) -v0.4.0 同时更新了**二进制**和**插件资产**(hooks 协议文本、skills),两者都要升级才能获得完整效果: +v0.4.0 同时更新了**二进制**和**插件资产**(hooks 协议文本、skills),两者都要升级才能获得完整效果。**最简单:重跑一键安装脚本**,二进制和插件一次到位: + +```bash +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash +``` + +手动路径(等价): ```bash # 1) 更新二进制(重复安装步骤下载 v0.4.0 包,覆盖旧二进制即可) -# 2) 更新插件(hooks + skills)。最简单:重跑一键接入(幂等,会重装插件并修正 MCP 配置) +# 2) 更新插件(hooks + skills)。重跑一键接入(幂等,会重装插件并修正 MCP 配置) engram setup claude-code - -# 或者手动刷新插件市场再更新: -# claude plugin marketplace update engram -# claude plugin update engram@engram +# 再刷新插件资产(plugin install 对已装插件是 no-op,不会拉新 hooks/skills): +claude plugin marketplace update engram +claude plugin update engram@engram # 3) 重启 Claude Code 加载新插件 ``` diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 3923197d..8862e054 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -2,7 +2,7 @@ # Installation -- [Homebrew (macOS / Linux)](#homebrew-macos--linux) +- [Quick install (macOS / Linux)](#quick-install-macos--linux) - [Windows](#windows) - [Install from source (macOS / Linux)](#install-from-source-macos--linux) - [Download binary (all platforms)](#download-binary-all-platforms) @@ -12,49 +12,43 @@ --- -## Homebrew (macOS / Linux) +## Quick install (macOS / Linux) -```bash -brew install gentleman-programming/tap/engram -``` - -Upgrade to latest: +One command installs the latest binary AND wires the Claude Code plugin +(this fork ships no Homebrew tap — the tap in older docs belongs to upstream): ```bash -brew update && brew upgrade engram +curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash ``` -> **Migrating from Cask?** If you installed engram before v1.0.1, it was distributed as a Cask. Uninstall first, then reinstall: -> ```bash -> brew uninstall --cask engram 2>/dev/null; brew install gentleman-programming/tap/engram -> ``` - -> **Keep `engram serve` running across `brew upgrade`?** On macOS, `brew upgrade engram` replaces the binary and kills any running `engram serve` process — autosync stops silently until you relaunch it. To make autosync survive upgrades and reboots, use the launchd template in [Running as a Service → Using launchd (macOS)](../DOCS.md#using-launchd-macos). Run `engram cloud status` afterwards: the `Local daemon:` line should report `running`. +The script auto-discovers the latest release, verifies the sha256 checksum, +installs to `~/.local/bin/engram`, and runs `engram setup claude-code`. +Upgrade = re-run the same command. Options go after `bash -s --`: +`--version X.Y.Z`, `--dir `, `--no-plugin`, `--force`, `--dry-run`, +`--protocol slim`. The script supports macOS/Linux (amd64/arm64) only — +Windows users see below. --- ## Windows -**Option A: Install via `go install` (recommended for technical users)** - -If you have Go installed, this is the cleanest and most trustworthy path — the binary is compiled on your machine from source, so no antivirus will flag it: +**Option A: Build from source (recommended for technical users)** -```powershell -go install github.com/Gentleman-Programming/engram/cmd/engram@latest -# Binary goes to %GOPATH%\bin\engram.exe (typically %USERPROFILE%\go\bin\) -``` +If you have Go installed, this is the cleanest and most trustworthy path — the binary is compiled on your machine from source, so no antivirus will flag it. -Ensure `%GOPATH%\bin` (or `%USERPROFILE%\go\bin`) is on your `PATH`. - -**Option B: Build from source** +> Do NOT use `go install github.com/...@latest` for this fork: the Go module +> path still points at the upstream repo, so `go install` would fetch +> upstream code, not this fork. Clone and build instead: ```powershell -git clone https://github.com/Gentleman-Programming/engram.git +git clone https://github.com/relytcloud/engram.git cd engram go install ./cmd/engram # Binary goes to %GOPATH%\bin\engram.exe (typically %USERPROFILE%\go\bin\) ``` +Ensure `%GOPATH%\bin` (or `%USERPROFILE%\go\bin`) is on your `PATH`. + > **Want a real version string instead of `dev`?** > > `go install` always stamps the binary as `dev`. To get a meaningful version, pick one of these — not both. Running them both leaves two binaries on disk and `engram version` keeps reporting `dev` because PATH still resolves to the `go install` build. @@ -78,7 +72,7 @@ go install ./cmd/engram **Option C: Download the prebuilt binary** -1. Go to [GitHub Releases](https://github.com/Gentleman-Programming/engram/releases) +1. Go to [GitHub Releases](https://github.com/relytcloud/engram/releases) 2. Download `engram__windows_amd64.zip` (or `arm64` for ARM devices) 3. Extract `engram.exe` to a folder in your `PATH` (e.g. `C:\Users\\bin\`) @@ -120,7 +114,7 @@ Expand-Archive engram_*_windows_amd64.zip -DestinationPath "$env:USERPROFILE\bin ## Install from source (macOS / Linux) ```bash -git clone https://github.com/Gentleman-Programming/engram.git +git clone https://github.com/relytcloud/engram.git cd engram go install ./cmd/engram # Binary goes to $GOPATH/bin (typically ~/go/bin/) @@ -149,7 +143,7 @@ go install ./cmd/engram ## Download binary (all platforms) -Grab the latest release for your platform from [GitHub Releases](https://github.com/Gentleman-Programming/engram/releases). +Grab the latest release for your platform from [GitHub Releases](https://github.com/relytcloud/engram/releases). | Platform | File | |----------|------| @@ -164,7 +158,7 @@ Grab the latest release for your platform from [GitHub Releases](https://github. ## Requirements -- **Go 1.24+** to build from source (not needed if installing via Homebrew or downloading a binary) +- **Go 1.25+** to build from source (not needed if using the quick-install script or downloading a binary) - That's it. No runtime dependencies. The binary includes SQLite (via [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) — pure Go, no CGO). Works natively on **macOS**, **Linux**, and **Windows** (x86_64 and ARM64). diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..c9090943 --- /dev/null +++ b/install.sh @@ -0,0 +1,461 @@ +#!/usr/bin/env bash +# engram installer/upgrader — binary + Claude Code plugin, in one command. +# +# curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh | bash +# curl -fsSL https://raw.githubusercontent.com/relytcloud/engram/main/install.sh \ +# | bash -s -- --dir /usr/local/bin --no-plugin +# +# What it does: +# 1. discovers the LATEST release automatically (no hardcoded version), +# 2. downloads the right archive for your OS/arch and verifies its sha256, +# 3. installs the binary (default: ~/.local/bin/engram), +# 4. wires the Claude Code plugin (marketplace + plugin + MCP config) and +# refreshes plugin assets on upgrades. +# +# Flags (pass after `bash -s --` when piping): +# --version pin a specific release instead of the latest +# --dir install directory (default: ~/.local/bin) +# --no-plugin skip the Claude Code plugin step +# --force reinstall even if the same version is present +# --no-verify skip sha256 verification (not recommended) +# --protocol set the memory-protocol verbosity during setup +# --replace-marketplace replace a foreign 'engram' plugin marketplace +# --yes never prompt (headless; allowlist step declined) +# --dry-run print what would happen, change nothing +# --help this text +# +# Env equivalents: ENGRAM_VERSION, ENGRAM_INSTALL_DIR, ENGRAM_NO_PLUGIN=1, +# ENGRAM_INSTALL_NO_VERIFY=1, ENGRAM_INSTALL_YES=1, GH_TOKEN/GITHUB_TOKEN +# (for the API fallback), NO_COLOR. Flags win over env. +# +# NOTE (curl|bash): bash reads THIS SCRIPT from fd 0 while executing it. +# Never `exec 0&2; } +step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*" >&2; } +warn() { printf '%swarning:%s %s\n' "$YELLOW" "$RESET" "$*" >&2; } +die() { + printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2 + exit 1 +} + +usage() { + # The flag reference lives in the header comment; print that block. + sed -n '2,32p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' >&2 || + info "engram installer — see https://github.com/${REPO}#install" +} + +cleanup() { [ -n "$WORKDIR" ] && rm -rf "$WORKDIR"; } + +make_workdir() { + WORKDIR=$(mktemp -d 2>/dev/null || mktemp -d -t engram-install) || + die "could not create a temporary directory" + trap cleanup EXIT + trap 'cleanup; exit 130' INT + trap 'cleanup; exit 143' TERM +} + +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --version) + [ $# -ge 2 ] || die "--version requires a value (e.g. --version 0.4.0)" + VERSION="$2" + shift 2 + ;; + --dir) + [ $# -ge 2 ] || die "--dir requires a path" + INSTALL_DIR="$2" + shift 2 + ;; + --protocol) + [ $# -ge 2 ] || die "--protocol requires slim or full" + case "$2" in + slim | full) PROTOCOL="$2" ;; + *) die "--protocol must be slim or full, got: $2" ;; + esac + shift 2 + ;; + --no-plugin) WITH_PLUGIN=0; shift ;; + --force) FORCE=1; shift ;; + --no-verify) VERIFY=0; shift ;; + --replace-marketplace) REPLACE_MARKETPLACE=1; shift ;; + --yes | -y) ASSUME_YES=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + --help | -h) + usage + exit 0 + ;; + *) + usage + die "unknown flag: $1" + ;; + esac + done +} + +require_curl() { + command -v curl >/dev/null 2>&1 || + die "curl is required (this installer is curl-based by design)" +} + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + linux | darwin) ;; + mingw* | msys* | cygwin* | windows*) + die "Windows is not supported by this script — see https://github.com/${REPO}/blob/main/docs/INSTALLATION.md#windows" + ;; + *) die "unsupported OS: ${os} (engram ships linux and darwin builds)" ;; + esac + case "$arch" in + x86_64 | amd64) arch=amd64 ;; + aarch64 | arm64) arch=arm64 ;; + *) die "unsupported architecture: ${arch} (engram ships amd64 and arm64)" ;; + esac + # An x86_64 shell under Rosetta on Apple Silicon should get the native build. + if [ "$os" = darwin ] && [ "$arch" = amd64 ] && + [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = "1" ]; then + arch=arm64 + fi + printf '%s_%s\n' "$os" "$arch" +} + +latest_version() { + local url tag token json + # Primary: follow the /releases/latest redirect — no API rate limit. + url=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ + --proto '=https' --tlsv1.2 --retry 2 --retry-delay 1 --max-time 20 \ + "${RELEASES}/latest" 2>/dev/null) || url="" + tag=${url##*/tag/} + if [ -n "$url" ] && [ "$tag" != "$url" ] && [ -n "$tag" ]; then + printf '%s\n' "${tag#v}" + return 0 + fi + # Fallback: GitHub API (rate-limited unauthenticated; honors GH_TOKEN). + # Two branches instead of a conditional array: empty-array expansion under + # `set -u` breaks on macOS bash 3.2. + token="${GH_TOKEN:-${GITHUB_TOKEN:-}}" + if [ -n "$token" ]; then + json=$(curl -fsSL --proto '=https' --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H "Authorization: Bearer ${token}" "$API_LATEST" 2>/dev/null) || json="" + else + json=$(curl -fsSL --proto '=https' --max-time 20 \ + -H 'Accept: application/vnd.github+json' "$API_LATEST" 2>/dev/null) || json="" + fi + tag=$(printf '%s' "$json" | + sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) + [ -n "$tag" ] || die "could not determine the latest engram version. + Both discovery paths failed (release redirect + GitHub API). Likely causes: + no network, an HTTPS proxy that blocks redirects, or API rate limiting + (set GH_TOKEN to authenticate the fallback). + Workaround: pin a version explicitly, e.g. --version 0.4.0 + Releases: ${RELEASES}" + printf '%s\n' "${tag#v}" +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$1" | awk '{print $NF}' + else + return 1 + fi +} + +# $1=archive path $2=asset name $3=checksums.txt path +verify_checksum() { + local expected actual + # goreleaser writes " ". awk exact-match is portable; + # `--ignore-missing` is not (BSD shasum lacks it). + expected=$(awk -v f="$2" '$2 == f { print $1 }' "$3") + [ -n "$expected" ] || die "no checksum entry for ${2} in checksums.txt" + actual=$(sha256_of "$1") || + die "no sha256 tool found (sha256sum / shasum / openssl); re-run with --no-verify to skip verification" + [ "$expected" = "$actual" ] && return 0 + die "checksum mismatch for ${2} + expected ${expected} + actual ${actual} + Refusing to install a corrupted download. Retry; if this persists, open an + issue: https://github.com/${REPO}/issues" +} + +# Prints the installed version of $1, or "" when absent/not runnable. +installed_version() { + local bin="$1" + if [ -z "$bin" ] || [ ! -x "$bin" ]; then + printf '' + return 0 + fi + # `engram version` may print an update notice on stderr first — drop it, + # and never let the child touch our stdin. + "$bin" version 2>/dev/null /dev/null || + die "cannot create ${INSTALL_DIR} — pass --dir " + [ -w "$INSTALL_DIR" ] || die "${INSTALL_DIR} is not writable. + Either pick a user directory: --dir \"\$HOME/.local/bin\" + or install system-wide: curl -fsSL | sudo bash -s -- --dir /usr/local/bin" + + # install(1) replaces the inode, so a running `engram serve` keeps its old + # image safely. + if ! install -m 0755 "${WORKDIR}/engram" "$target" 2>/dev/null; then + if ! cp "${WORKDIR}/engram" "$target" || ! chmod 0755 "$target"; then + die "could not install to ${target}" + fi + fi + + # macOS binaries are ad-hoc signed, not notarized — strip quarantine. + if [ "$(uname -s)" = Darwin ] && command -v xattr >/dev/null 2>&1; then + xattr -d com.apple.quarantine "$target" >/dev/null 2>&1 || true + fi + printf '%s\n' "$target" +} + +tty_available() { [ -e /dev/tty ] && (exec 3/dev/null; } + +# y/N on the real terminal; defaults to N when headless or --yes. +confirm() { + local answer="" + # --yes must never auto-approve destroying someone else's marketplace. + [ "$ASSUME_YES" = 1 ] && return 1 + tty_available || return 1 + printf '%s [y/N] ' "$1" >&2 + read -r answer /dev/null | + sed $'s/\033\\[[0-9;]*[A-Za-z]//g' | + awk -v want="$1" ' + /Source:/ { + if (found && match($0, /\(([^)]+)\)/)) { + print substr($0, RSTART + 1, RLENGTH - 2) + exit + } + next + } + { + line = $0 + sub(/^[^A-Za-z0-9_.-]*/, "", line) + sub(/[[:space:]]+$/, "", line) + if (line == want) found = 1 + }' +} + +# $1 = absolute path of the just-installed binary +setup_claude_code() { + local bin="$1" src + if ! command -v claude >/dev/null 2>&1; then + warn "claude CLI not found — skipping the Claude Code plugin step. + Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code + Then wire the plugin with: ${bin} setup claude-code" + return 0 + fi + + src=$(claude_marketplace_source engram || true) + if [ -n "$src" ] && + [ "$(printf '%s' "$src" | tr '[:upper:]' '[:lower:]')" != "$(printf '%s' "$MARKETPLACE" | tr '[:upper:]' '[:lower:]')" ]; then + if [ "$REPLACE_MARKETPLACE" = 1 ] || + confirm "Plugin marketplace 'engram' currently points at ${src}. Replace it with ${MARKETPLACE}?"; then + step "replacing the 'engram' marketplace (${src} -> ${MARKETPLACE})" + claude plugin marketplace remove engram /dev/null 2>&1 || + warn "could not remove the existing 'engram' marketplace" + else + warn "plugin step skipped — a different 'engram' marketplace (${src}) is configured. + To switch to this fork's plugin, run: + claude plugin marketplace remove engram + ${bin} setup claude-code + (or re-run this installer with --replace-marketplace)" + return 0 + fi + fi + + step "wiring the Claude Code plugin (marketplace + plugin + MCP config)" + # Pass ONLY known args: an unknown flag makes `engram setup` fall back to + # its INTERACTIVE menu, which reads stdin (fatal under curl|bash). + set -- setup claude-code + [ -n "$PROTOCOL" ] && set -- "$@" "--protocol=${PROTOCOL}" + + if [ -t 0 ]; then + "$bin" "$@" || + warn "engram setup claude-code failed — re-run it manually to finish plugin setup" + elif [ "$ASSUME_YES" = 0 ] && tty_available; then + # Under curl|bash stdin is the script pipe; borrow the real terminal so + # the allowlist (y/N) prompt still works and no script bytes are eaten. + "$bin" "$@" /dev/null 2>&1 || + warn "could not refresh the engram plugin marketplace (will refresh on next Claude Code restart)" + claude plugin update engram@engram /dev/null 2>&1 || true + info " plugin wired. Restart Claude Code to load the updated plugin." +} + +# $1 = installed target path +report_path() { + local target="$1" resolved + case ":${PATH}:" in + *":${INSTALL_DIR}:"*) ;; + *) + warn "${INSTALL_DIR} is not on your PATH. Add it:" + case "$(basename "${SHELL:-sh}")" in + zsh) info " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.zshrc && exec zsh" ;; + bash) + info " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.bashrc && exec bash" + [ "$(uname -s)" = Darwin ] && + info " (macOS login shells may read ~/.bash_profile instead)" + ;; + fish) info " fish_add_path ${INSTALL_DIR}" ;; + *) info " export PATH=\"${INSTALL_DIR}:\$PATH\" # add to your shell's rc file" ;; + esac + ;; + esac + resolved=$(command -v engram 2>/dev/null || true) + if [ -n "$resolved" ] && [ "$resolved" != "$target" ]; then + warn "another engram is earlier in PATH: ${resolved} (version: $(installed_version "$resolved")) + ${target} was just installed. Remove the older copy or reorder PATH, then + re-run '${target} setup claude-code' so the MCP config points at the right binary." + fi +} + +main() { + parse_args "$@" + require_curl + + local plat target have ver + plat=$(detect_platform) + target="${INSTALL_DIR}/engram" + have=$(installed_version "$target") + [ -z "$have" ] && have=$(installed_version "$(command -v engram 2>/dev/null || true)") + + ver="${VERSION#v}" + if [ -z "$ver" ]; then + step "resolving the latest release" + ver=$(latest_version) + fi + + if [ "$DRY_RUN" = 1 ]; then + info "dry run — nothing will be changed" + info " platform: ${plat}" + info " version: ${ver}$([ -n "$have" ] && printf ' (installed: %s)' "$have")" + info " asset: ${RELEASES}/download/v${ver}/engram_${ver}_${plat}.tar.gz" + info " install to: ${target}" + info " plugin: $([ "$WITH_PLUGIN" = 1 ] && printf 'yes (Claude Code)' || printf 'no (--no-plugin)')" + exit 0 + fi + + make_workdir + + if [ -n "$have" ] && [ "$have" = "$ver" ] && [ "$FORCE" != 1 ]; then + info "engram ${ver} is already installed at ${target} — skipping download (--force to reinstall)" + else + if [ -n "$have" ]; then + step "upgrading engram ${have} -> ${ver}" + else + step "installing engram ${ver}" + fi + target=$(install_binary "$ver" "$plat") + fi + + "$target" version /dev/null 2>&1 || + die "the installed binary does not run. + macOS Gatekeeper? Try: xattr -d com.apple.quarantine ${target} + Otherwise see ${RELEASES}" + + if [ "$WITH_PLUGIN" = 1 ]; then + setup_claude_code "$target" + else + info "plugin step skipped (--no-plugin)" + fi + + report_path "$target" + + step "done — engram $("$target" version /dev/null | awk '{print $2}') at ${target}" + info " next steps:" + info " restart Claude Code (loads the plugin + MCP tools)" + info " engram version # verify" + info " engram doctor # health check" +} + +main "$@"