#!/bin/sh # install-r2.sh — one-line difyctl installer from Cloudflare R2. # Reads a per-channel pointer manifest, sha256-verifies, installs to PATH. # Usage: curl -fsSL https://raw.githubusercontent.com/langgenius/dify/main/cli/scripts/install-r2.sh | DIFYCTL_R2_BASE= sh # Env: # DIFYCTL_R2_BASE (required) R2 public base, e.g. https://pub-….r2.dev # DIFYCTL_CHANNEL (default edge) # DIFYCTL_INSTALL_DIR (default $HOME/.local/bin) directory the binary is written to as /difyctl # DIFYCTL_VERSION pin an exact published version (e.g. 0.1.0-edge.ce4af868) # DIFYCTL_COMMIT pin by git commit (short or full sha); resolved via index.json # DIFYCTL_R2_PREFIX (default difyctl) key root for pointer JSONs # DIFYCTL_R2_BIN_PREFIX (default /bin) key root for binaries # With no pin the channel pointer (latest) is installed. A pin resolves through # //index.json -> the build's immutable dir under the bin prefix. set -eu # --- library functions (sourced for tests when DIFYCTL_INSTALL_LIB=1) --- tmp_m="$(mktemp 2>/dev/null || echo /tmp/difyctl-manifest.$$)" trap 'rm -f "$tmp_m" "${tmp_c:-}" "${tmp_b:-}"' EXIT INT TERM err() { printf '%s\n' "install-r2: $*" >&2; } die() { err "$*"; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; } detect_target() { case "$(uname -s)" in Linux*) _os=linux ;; Darwin*) _os=darwin ;; *) die "unsupported OS: $(uname -s) (use install.ps1 on Windows)" ;; esac case "$(uname -m)" in x86_64|amd64) _arch=x64 ;; arm64|aarch64) _arch=arm64 ;; *) die "unsupported arch: $(uname -m)" ;; esac printf '%s-%s' "$_os" "$_arch" } # grep/sed (no jq). Correct only because release-r2-edge.mjs renders one key per line. # manifest_str -> value of a top-level string field manifest_str() { grep "\"$2\"[[:space:]]*:" "$1" | head -1 \ | sed -E "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\\1/" } # manifest_target_field -> value on that target's line manifest_target_field() { grep "\"$2\"[[:space:]]*:" "$1" | head -1 \ | sed -E "s/.*\"$3\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\\1/" } # Resolve a pinned build from index.json (no jq). Correct only because # release-r2-edge.mjs renders each build's fields one per line, dir last. # Prints "\t" of the first match, nothing if none. # index_resolve (commit = prefix match) index_resolve() { awk -v kind="$2" -v want="$3" ' function val(s) { sub(/^[^:]*:[[:space:]]*"/, "", s); sub(/".*$/, "", s); return s } /"version"[[:space:]]*:/ { v = val($0) } /"commit"[[:space:]]*:/ { c = val($0) } /"dir"[[:space:]]*:/ { d = val($0) sel = (kind == "commit") ? c : v if (kind == "commit") { if (index(sel, want) == 1) { print v "\t" d; exit } } else if (sel == want) { print v "\t" d; exit } } ' "$1" } # checksums_target -> "\t" # checksums lines are " "; match asset ending - or -.exe. checksums_target() { grep -E "[[:space:]]difyctl-v.*-$2(\.exe)?\$" "$1" | head -1 \ | awk '{ print $1 "\t" $NF }' } sha256_check() { # $1 = file, $2 = expected hex if command -v sha256sum >/dev/null 2>&1; then _a="$(sha256sum "$1" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then _a="$(shasum -a 256 "$1" | awk '{print $1}')" else die "no sha256 tool (need sha256sum or shasum)"; fi [ "$_a" = "$2" ] || die "checksum mismatch for $1" } # fetch_verify_install fetch_verify_install() { tmp_b="$(mktemp)" # NOTE: no --compressed — must hash the raw bytes curl -fsSL "$1" -o "$tmp_b" || die "download failed: $1" sha256_check "$tmp_b" "$2" mkdir -p "$install_dir" chmod +x "$tmp_b" mv "$tmp_b" "${install_dir}/difyctl" 2>/dev/null || { cp "$tmp_b" "${install_dir}/difyctl"; rm -f "$tmp_b"; } printf 'difyctl %s (channel %s) installed: %s\n' "$3" "$4" "${install_dir}/difyctl" case ":${PATH}:" in *":${install_dir}:"*) ;; *) printf 'note: add %s to your PATH\n' "$install_dir" ;; esac } # Resolve a pinned build into download url + sha. Sets: version, dl_url, dl_sha. resolve_pinned() { iurl="${base}/${prefix}/${channel}/index.json" curl -fsSL "$iurl" -o "$tmp_m" || die "R2 unavailable fetching ${iurl}; retry." if [ -n "${DIFYCTL_VERSION:-}" ]; then res="$(index_resolve "$tmp_m" version "$DIFYCTL_VERSION")" else res="$(index_resolve "$tmp_m" commit "$DIFYCTL_COMMIT")"; fi [ -n "$res" ] || die "no build matching ${DIFYCTL_VERSION:-$DIFYCTL_COMMIT} in channel ${channel}" version="$(printf '%s' "$res" | cut -f1)" dir="$(printf '%s' "$res" | cut -f2)" vbase="${base}/${bin_prefix}/${channel}/${dir}" tmp_c="$(mktemp)" curl -fsSL "${vbase}/difyctl-v${version}-checksums.txt" -o "$tmp_c" \ || die "checksums missing for ${version} (channel ${channel})" line="$(checksums_target "$tmp_c" "$target")" [ -n "$line" ] || die "no build for ${target} at ${version}" dl_sha="$(printf '%s' "$line" | cut -f1)" dl_url="${vbase}/$(printf '%s' "$line" | cut -f2)" } # Resolve the channel pointer (latest) into download url + sha. Sets the same. resolve_pointer() { murl="${base}/${prefix}/${channel}/manifest.json" _code="$(curl -fsS -o "$tmp_m" -w '%{http_code}' "$murl" 2>/dev/null || true)" if [ ! -s "$tmp_m" ]; then case "$_code" in 404) die "channel '${channel}' not published to R2. For rc/stable use the GitHub installer (install-cli.sh)." ;; *) die "R2 unavailable (HTTP ${_code:-?}) fetching ${murl}; retry." ;; esac fi mchannel="$(manifest_str "$tmp_m" channel)" [ "$mchannel" = "$channel" ] || die "manifest channel '${mchannel}' != requested '${channel}'" version="$(manifest_str "$tmp_m" version)" baseUrl="$(manifest_str "$tmp_m" baseUrl)" asset="$(manifest_target_field "$tmp_m" "$target" asset)" dl_sha="$(manifest_target_field "$tmp_m" "$target" sha256)" [ -n "$asset" ] && [ -n "$dl_sha" ] || die "no build for ${target} in channel ${channel}" dl_url="${baseUrl}/${asset}" } install_main() { need curl [ -n "${DIFYCTL_R2_BASE:-}" ] || die "set DIFYCTL_R2_BASE to the R2 public base (e.g. https://pub-….r2.dev)" base="${DIFYCTL_R2_BASE%/}" channel="${DIFYCTL_CHANNEL:-edge}" prefix="${DIFYCTL_R2_PREFIX:-difyctl}" bin_prefix="${DIFYCTL_R2_BIN_PREFIX:-${prefix}/bin}" install_dir="${DIFYCTL_INSTALL_DIR:-${HOME}/.local/bin}" target="$(detect_target)" if [ -n "${DIFYCTL_VERSION:-}" ] || [ -n "${DIFYCTL_COMMIT:-}" ]; then resolve_pinned else resolve_pointer fi fetch_verify_install "$dl_url" "$dl_sha" "$version" "$channel" } if [ "${DIFYCTL_INSTALL_LIB:-0}" != "1" ]; then install_main fi