|
| 1 | +#!/bin/sh |
| 2 | +# ensure-flowctl.sh — idempotent binary provisioner for the flow-code plugin. |
| 3 | +# |
| 4 | +# Called from SessionStart hook. Ensures $PLUGIN_ROOT/bin/flowctl exists and |
| 5 | +# matches the version pinned in .claude-plugin/flowctl-version. |
| 6 | +# |
| 7 | +# Strategy: |
| 8 | +# 1. Fast path: binary present and version matches → exit 0 in <100ms |
| 9 | +# 2. Download from GitHub Releases (with checksum verification) |
| 10 | +# 3. Fallback: cargo build from source (if toolchain + source available) |
| 11 | +# 4. On any failure: warn but exit 0 (never block session start) |
| 12 | +# |
| 13 | +# Env overrides: |
| 14 | +# FLOWCTL_SKIP_ENSURE=1 — skip entirely (for CI/testing) |
| 15 | +# FLOWCTL_FORCE_BUILD=1 — skip download, build from source directly |
| 16 | + |
| 17 | +set -eu |
| 18 | + |
| 19 | +[ "${FLOWCTL_SKIP_ENSURE:-0}" = "1" ] && exit 0 |
| 20 | + |
| 21 | +PLUGIN_ROOT="${DROID_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}" |
| 22 | +[ -n "$PLUGIN_ROOT" ] || exit 0 |
| 23 | +[ -d "$PLUGIN_ROOT" ] || exit 0 |
| 24 | + |
| 25 | +PINNED_FILE="$PLUGIN_ROOT/.claude-plugin/flowctl-version" |
| 26 | +[ -f "$PINNED_FILE" ] || exit 0 |
| 27 | + |
| 28 | +PINNED="$(tr -d ' \t\n\r' < "$PINNED_FILE")" |
| 29 | +[ -n "$PINNED" ] || exit 0 |
| 30 | + |
| 31 | +BIN="$PLUGIN_ROOT/bin/flowctl" |
| 32 | + |
| 33 | +# Fast path: binary exists and version matches |
| 34 | +if [ -x "$BIN" ] && [ "${FLOWCTL_FORCE_BUILD:-0}" != "1" ]; then |
| 35 | + CURRENT="$("$BIN" --version 2>/dev/null | awk 'NR==1{print $2}' || echo "")" |
| 36 | + if [ -n "$CURRENT" ]; then |
| 37 | + # PINNED may be "v0.1.26" or "0.1.26"; normalize |
| 38 | + PINNED_NUM="${PINNED#v}" |
| 39 | + if [ "$CURRENT" = "$PINNED_NUM" ]; then |
| 40 | + exit 0 |
| 41 | + fi |
| 42 | + fi |
| 43 | +fi |
| 44 | + |
| 45 | +# Need to provision |
| 46 | +log() { printf '[flow-code] %s\n' "$1" >&2; } |
| 47 | + |
| 48 | +log "provisioning flowctl $PINNED..." |
| 49 | + |
| 50 | +mkdir -p "$PLUGIN_ROOT/bin" |
| 51 | + |
| 52 | +# ── Try GitHub Release download ───────────────────────────────────── |
| 53 | +try_download() { |
| 54 | + OS="$(uname -s)" |
| 55 | + ARCH="$(uname -m)" |
| 56 | + case "$OS" in |
| 57 | + Linux) OS_T="unknown-linux-gnu" ;; |
| 58 | + Darwin) OS_T="apple-darwin" ;; |
| 59 | + MINGW*|MSYS*|CYGWIN*) OS_T="pc-windows-msvc" ;; |
| 60 | + *) log "unsupported OS: $OS"; return 1 ;; |
| 61 | + esac |
| 62 | + case "$ARCH" in |
| 63 | + x86_64|amd64) ARCH_T="x86_64" ;; |
| 64 | + aarch64|arm64) ARCH_T="aarch64" ;; |
| 65 | + *) log "unsupported arch: $ARCH"; return 1 ;; |
| 66 | + esac |
| 67 | + PLATFORM="${ARCH_T}-${OS_T}" |
| 68 | + |
| 69 | + case "$OS_T" in |
| 70 | + pc-windows-msvc) EXT="zip"; BIN_NAME="flowctl.exe" ;; |
| 71 | + *) EXT="tar.gz"; BIN_NAME="flowctl" ;; |
| 72 | + esac |
| 73 | + |
| 74 | + REPO="z23cc/flow-code" |
| 75 | + ARCHIVE="flowctl-${PINNED}-${PLATFORM}.${EXT}" |
| 76 | + URL="https://github.com/${REPO}/releases/download/${PINNED}/${ARCHIVE}" |
| 77 | + |
| 78 | + command -v curl >/dev/null 2>&1 || { log "curl not found"; return 1; } |
| 79 | + |
| 80 | + TMP="$(mktemp -d)" |
| 81 | + # shellcheck disable=SC2064 |
| 82 | + trap "rm -rf '$TMP'" EXIT INT TERM |
| 83 | + |
| 84 | + log "downloading $ARCHIVE" |
| 85 | + if ! curl -fsSL --connect-timeout 10 --max-time 120 "$URL" -o "$TMP/$ARCHIVE"; then |
| 86 | + log "download failed: $URL" |
| 87 | + return 1 |
| 88 | + fi |
| 89 | + |
| 90 | + # Verify checksum if available (best-effort) |
| 91 | + if curl -fsSL --connect-timeout 5 --max-time 15 "${URL}.sha256" -o "$TMP/$ARCHIVE.sha256" 2>/dev/null; then |
| 92 | + (cd "$TMP" && { |
| 93 | + if command -v sha256sum >/dev/null 2>&1; then |
| 94 | + sha256sum -c "$ARCHIVE.sha256" >/dev/null 2>&1 |
| 95 | + elif command -v shasum >/dev/null 2>&1; then |
| 96 | + shasum -a 256 -c "$ARCHIVE.sha256" >/dev/null 2>&1 |
| 97 | + else |
| 98 | + true |
| 99 | + fi |
| 100 | + }) || { log "checksum verification failed"; return 1; } |
| 101 | + fi |
| 102 | + |
| 103 | + # Extract |
| 104 | + case "$EXT" in |
| 105 | + tar.gz) |
| 106 | + tar xzf "$TMP/$ARCHIVE" -C "$TMP" || { log "extract failed"; return 1; } |
| 107 | + ;; |
| 108 | + zip) |
| 109 | + (cd "$TMP" && unzip -q "$ARCHIVE") || { log "extract failed"; return 1; } |
| 110 | + ;; |
| 111 | + esac |
| 112 | + |
| 113 | + [ -f "$TMP/$BIN_NAME" ] || { log "archive missing $BIN_NAME"; return 1; } |
| 114 | + |
| 115 | + mv "$TMP/$BIN_NAME" "$BIN" |
| 116 | + chmod +x "$BIN" |
| 117 | + log "flowctl $PINNED installed from GitHub Release ✓" |
| 118 | + return 0 |
| 119 | +} |
| 120 | + |
| 121 | +# ── Fallback: build from source ───────────────────────────────────── |
| 122 | +try_build() { |
| 123 | + SRC="$PLUGIN_ROOT/flowctl" |
| 124 | + [ -d "$SRC/crates" ] || { log "source not available at $SRC"; return 1; } |
| 125 | + command -v cargo >/dev/null 2>&1 || { log "cargo not found"; return 1; } |
| 126 | + |
| 127 | + log "building from source (may take ~1-2min, downloads ~130MB on first run)..." |
| 128 | + (cd "$SRC" && cargo build --release -p flowctl-cli >&2) || { log "cargo build failed"; return 1; } |
| 129 | + |
| 130 | + BUILT="$SRC/target/release/flowctl" |
| 131 | + [ -x "$BUILT" ] || { log "built binary not found at $BUILT"; return 1; } |
| 132 | + |
| 133 | + cp "$BUILT" "$BIN" |
| 134 | + chmod +x "$BIN" |
| 135 | + log "flowctl built from source ✓" |
| 136 | + return 0 |
| 137 | +} |
| 138 | + |
| 139 | +# ── Execute strategy ──────────────────────────────────────────────── |
| 140 | +if [ "${FLOWCTL_FORCE_BUILD:-0}" = "1" ]; then |
| 141 | + try_build || log "build failed — flow-code features disabled" |
| 142 | +else |
| 143 | + try_download || try_build || { |
| 144 | + log "flowctl unavailable — flow-code features will be disabled" |
| 145 | + log "retry manually: $PLUGIN_ROOT/scripts/hooks/ensure-flowctl.sh" |
| 146 | + } |
| 147 | +fi |
| 148 | + |
| 149 | +exit 0 |
0 commit comments