11#! /bin/sh
22# Flashduty CLI installer
33# Usage: curl -sSL https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.sh | sh
4+ #
5+ # Environment:
6+ # FLASHDUTY_VERSION Install a specific version (e.g. v0.1.2). Default: latest.
7+ # FLASHDUTY_INSTALL_DIR Install directory. Default: /usr/local/bin.
8+ # MIRROR_URL Fetch release assets from this https mirror prefix
9+ # instead of github.com. The mirror must replicate
10+ # GitHub's release layout
11+ # (<MIRROR_URL>/releases/download/<tag>/<asset>) and expose
12+ # a plain-text <MIRROR_URL>/releases/latest file containing
13+ # the latest tag.
414set -e
515
616REPO=" flashcatcloud/flashduty-cli"
717BINARY=" flashduty-cli"
818INSTALLED_NAME=" flashduty"
919INSTALL_DIR=" ${FLASHDUTY_INSTALL_DIR:-/ usr/ local/ bin} "
1020
21+ # When set, all release downloads are fetched from this prefix instead of github.com.
22+ MIRROR_URL=" ${MIRROR_URL:- } "
23+ MIRROR_URL=" ${MIRROR_URL%/ } "
24+ if [ -n " ${MIRROR_URL} " ]; then
25+ case " ${MIRROR_URL} " in
26+ https://* ) : ;;
27+ * ) printf " Error: MIRROR_URL must use https:// scheme, got: %s\n" " ${MIRROR_URL} " >&2 ; exit 1 ;;
28+ esac
29+ fi
30+
1131# --- helper functions ---
1232
1333fail () {
@@ -25,6 +45,17 @@ need_cmd() {
2545 fi
2646}
2747
48+ sha256_of () {
49+ file=" $1 "
50+ if command -v sha256sum > /dev/null 2>&1 ; then
51+ sha256sum " ${file} " | awk ' {print $1}'
52+ elif command -v shasum > /dev/null 2>&1 ; then
53+ shasum -a 256 " ${file} " | awk ' {print $1}'
54+ else
55+ fail " need 'sha256sum' or 'shasum' to verify the download (install coreutils)"
56+ fi
57+ }
58+
2859# --- detect platform ---
2960
3061detect_os () {
@@ -51,14 +82,30 @@ resolve_version() {
5182 echo " ${FLASHDUTY_VERSION} "
5283 return
5384 fi
54- need_cmd curl
55- # Use the GitHub API to get the latest release tag
56- version=$( curl -sL " https://api.github.com/repos/${REPO} /releases/latest" \
57- | grep ' "tag_name"' \
58- | sed -E ' s/.*"tag_name": *"([^"]+)".*/\1/' )
85+ if [ -n " ${MIRROR_URL} " ]; then
86+ # The mirror publishes a plain-text pointer with the latest tag.
87+ version=$( curl --proto ' =https' --tlsv1.2 -fsSL " ${MIRROR_URL} /releases/latest" 2> /dev/null \
88+ | awk ' NR==1 {gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' )
89+ else
90+ # Follow the github.com/<repo>/releases/latest redirect to read the tag
91+ # from the resolved URL — avoids the unauthenticated api.github.com rate limit.
92+ effective=$( curl --proto ' =https' --tlsv1.2 -sIL -o /dev/null -w ' %{url_effective}' \
93+ " https://github.com/${REPO} /releases/latest" || true)
94+ version=" ${effective##*/ } "
95+ [ " ${version} " = " latest" ] && version=" "
96+ fi
5997 if [ -z " ${version} " ]; then
6098 fail " could not determine latest version. Set FLASHDUTY_VERSION to install a specific version."
6199 fi
100+ # Reject anything that doesn't look like a release tag — the resolved value
101+ # comes from a network response and is interpolated into the download URL.
102+ case " ${version} " in
103+ * [!A-Za-z0-9.+-]* ) fail " resolved version contains illegal characters: '${version} '" ;;
104+ esac
105+ case " ${version} " in
106+ v[0-9]* ) : ;;
107+ * ) fail " resolved version is not a valid release tag: '${version} '" ;;
108+ esac
62109 echo " ${version} "
63110}
64111
@@ -81,17 +128,36 @@ main() {
81128 fi
82129
83130 ARCHIVE=" flashduty-cli_${OS} _${ARCH} .${EXT} "
84- URL=" https://github.com/${REPO} /releases/download/${VERSION} /${ARCHIVE} "
131+ if [ -n " ${MIRROR_URL} " ]; then
132+ BASE=" ${MIRROR_URL} /releases/download/${VERSION} "
133+ else
134+ BASE=" https://github.com/${REPO} /releases/download/${VERSION} "
135+ fi
85136
86137 info " Installing Flashduty CLI ${VERSION} (${OS} /${ARCH} )"
87- info " Downloading ${URL } "
138+ info " Downloading ${BASE} / ${ARCHIVE }"
88139
89140 TMP_DIR=$( mktemp -d)
90141 trap ' rm -rf "${TMP_DIR}"' EXIT
91142
92- HTTP_CODE=$( curl -sL -H " Accept: application/octet-stream" -o " ${TMP_DIR} /${ARCHIVE} " -w " %{http_code}" " ${URL} " )
93- if [ " ${HTTP_CODE} " != " 200" ]; then
94- fail " download failed (HTTP ${HTTP_CODE} ). Check that ${VERSION} exists at https://github.com/${REPO} /releases"
143+ if ! curl --proto ' =https' --tlsv1.2 -fsSL " ${BASE} /${ARCHIVE} " -o " ${TMP_DIR} /${ARCHIVE} " ; then
144+ fail " download failed for ${BASE} /${ARCHIVE} . Check that ${VERSION} exists."
145+ fi
146+
147+ # Verify against the published checksums.txt when present. Releases cut
148+ # before the mirror existed don't ship one, so a missing file only warns.
149+ if curl --proto ' =https' --tlsv1.2 -fsSL " ${BASE} /checksums.txt" -o " ${TMP_DIR} /checksums.txt" 2> /dev/null; then
150+ expected=$( awk -v a=" ${ARCHIVE} " ' $2 == a {print $1; exit}' " ${TMP_DIR} /checksums.txt" )
151+ if [ -z " ${expected} " ]; then
152+ fail " archive ${ARCHIVE} not listed in checksums.txt (wrong release or renamed asset)"
153+ fi
154+ actual=$( sha256_of " ${TMP_DIR} /${ARCHIVE} " )
155+ if [ " ${actual} " != " ${expected} " ]; then
156+ fail " checksum mismatch for ${ARCHIVE} : expected ${expected} , got ${actual} "
157+ fi
158+ info " Checksum OK"
159+ else
160+ info " WARNING: checksums.txt not available — skipping integrity check"
95161 fi
96162
97163 if [ " ${EXT} " = " zip" ]; then
0 commit comments