#!/bin/sh # DFlow CLI installer # # Usage: # curl -fsSL https://cli.dflow.net/install.sh | sh # # All options can be set via flags or environment variables. # Flags take precedence over environment variables. main() { # Enable POSIX word-splitting in zsh; silently ignored by other shells emulate sh 2>/dev/null || true set -eu BINARY_NAME="dflow" BASE_URL="${DFLOW_BASE_URL:-https://cli.dflow.net}" CURL_RETRY_OPTS="--retry 3 --retry-all-errors --retry-delay 2" SUPPORTED_TARGETS="aarch64-apple-darwin x86_64-apple-darwin \ x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu" BOLD="$(tput bold 2>/dev/null || printf '')" GREY="$(tput setaf 0 2>/dev/null || printf '')" UNDERLINE="$(tput smul 2>/dev/null || printf '')" RED="$(tput setaf 1 2>/dev/null || printf '')" GREEN="$(tput setaf 2 2>/dev/null || printf '')" YELLOW="$(tput setaf 3 2>/dev/null || printf '')" BLUE="$(tput setaf 4 2>/dev/null || printf '')" MAGENTA="$(tput setaf 5 2>/dev/null || printf '')" NO_COLOR="$(tput sgr0 2>/dev/null || printf '')" HELP_TEXT="DFlow CLI installer Usage: curl -fsSL https://cli.dflow.net/install.sh | sh Options: -v, --version Install a specific tagged release (e.g. 0.1.0) -c, --commit Pin to a specific commit (requires --branch) -b, --bin-dir Override the installation directory -p, --platform Override detected platform -a, --arch Override detected architecture -B, --base-url Override the base download URL --branch Install latest build from a branch (e.g. main) -V, --verbose Enable verbose output -f, -y, --force Skip the confirmation prompt -r, --remove Uninstall dflow -h, --help Show this help message Environment variables: DFLOW_VERSION Install a specific tagged release (e.g. 0.1.0) DFLOW_BRANCH Install latest build from a branch (e.g. main) DFLOW_COMMIT Pin to a specific commit (requires DFLOW_BRANCH) DFLOW_BIN_DIR Override installation directory DFLOW_BASE_URL Override base download URL Examples: # Install latest release curl -fsSL https://cli.dflow.net/install.sh | sh # Install a specific version curl -fsSL https://cli.dflow.net/install.sh | sh -s -- --version 0.1.0 # Install latest build from a branch curl -fsSL https://cli.dflow.net/install.sh | sh -s -- --branch main # Install a specific commit from a branch curl -fsSL https://cli.dflow.net/install.sh | sh -s -- --branch main --commit abc123 # Using environment variables DFLOW_VERSION=0.1.0 curl -fsSL https://cli.dflow.net/install.sh | sh " info() { printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*" } debug() { if [ -n "${VERBOSE:-}" ]; then printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*" fi } warn() { printf '%s\n' "${YELLOW}! $*${NO_COLOR}" } error() { printf '%s\n' "${RED}x $*${NO_COLOR}" >&2 } completed() { printf '%s\n' "${GREEN}✓${NO_COLOR} $*" } has() { command -v "$1" 1>/dev/null 2>&1 } test_writeable() { _tw_path="${1:-}/test.txt" if touch "${_tw_path}" 2>/dev/null; then rm "${_tw_path}" return 0 else return 1 fi } detect_platform() { _dp_platform="$(uname -s | tr '[:upper:]' '[:lower:]')" case "${_dp_platform}" in linux) _dp_platform="unknown-linux-gnu" ;; darwin) _dp_platform="apple-darwin" ;; *) error "Unsupported operating system: ${_dp_platform}" info "dflow is available for macOS and Linux." return 1 ;; esac printf '%s' "${_dp_platform}" } detect_arch() { _da_arch="$(uname -m | tr '[:upper:]' '[:lower:]')" case "${_da_arch}" in amd64|x86_64) printf 'x86_64' ;; arm64|aarch64) printf 'aarch64' ;; *) error "Unsupported architecture: ${_da_arch}" return 1 ;; esac } is_build_available() { _iba_target="$1" _iba_good="" for _iba_t in $SUPPORTED_TARGETS; do if [ "${_iba_t}" = "${_iba_target}" ]; then _iba_good=1 break fi done if [ "${_iba_good}" != "1" ]; then error "Builds for ${MAGENTA}${_iba_target}${NO_COLOR} are not available." info "Supported targets: ${SUPPORTED_TARGETS}" return 1 fi } resolve_download_url() { _rdu_version="${DFLOW_VERSION:-}" _rdu_branch="${DFLOW_BRANCH:-}" _rdu_commit="${DFLOW_COMMIT:-}" if [ -n "${_rdu_version}" ] && [ -n "${_rdu_branch}" ]; then error "Cannot specify both version and branch." return 1 fi if [ -n "${_rdu_commit}" ] && [ -z "${_rdu_branch}" ]; then warn "Commit is set but branch is not. Ignoring commit." _rdu_commit="" fi if [ -n "${_rdu_version}" ]; then _rdu_version="${_rdu_version#v}" PATH_PREFIX="tags/v${_rdu_version}" DISPLAY_VERSION="v${_rdu_version}" elif [ -n "${_rdu_branch}" ]; then _rdu_branch="$(printf '%s' "${_rdu_branch}" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')" if [ -n "${_rdu_commit}" ]; then PATH_PREFIX="branches/${_rdu_branch}/${_rdu_commit}" DISPLAY_VERSION="${_rdu_branch}@${_rdu_commit}" else PATH_PREFIX="branches/${_rdu_branch}/latest" DISPLAY_VERSION="${_rdu_branch}@latest" fi else PATH_PREFIX="latest" DISPLAY_VERSION="latest" fi URL="${BASE_URL}/${PATH_PREFIX}/${BINARY_NAME}-${TARGET}.tar.gz" } find_bin_dir() { if [ -n "${DFLOW_BIN_DIR:-}" ]; then printf '%s' "${DFLOW_BIN_DIR}" return fi # Prefer user-writable directories already on PATH _fbd_preferred="$HOME/.local/bin:$HOME/bin:$HOME/.bin" _fbd_old_ifs="${IFS:-}" IFS=':' for _fbd_candidate in $_fbd_preferred; do for _fbd_path_entry in $PATH; do if [ "$_fbd_candidate" = "$_fbd_path_entry" ] && [ -d "$_fbd_candidate" ] && test_writeable "$_fbd_candidate"; then IFS="$_fbd_old_ifs" printf '%s' "$_fbd_candidate" return fi done done # Check remaining PATH entries (skip system dirs) for _fbd_dir in $PATH; do case "$_fbd_dir" in /usr/bin|/bin|/usr/sbin|/sbin) continue ;; esac if [ -d "$_fbd_dir" ] && test_writeable "$_fbd_dir"; then IFS="$_fbd_old_ifs" printf '%s' "$_fbd_dir" return fi done IFS="$_fbd_old_ifs" # Try creating ~/.local/bin _fbd_local_bin="$HOME/.local/bin" if [ ! -d "$_fbd_local_bin" ]; then if mkdir -p "$_fbd_local_bin" 2>/dev/null; then printf '%s' "$_fbd_local_bin" return fi fi # Fall back to /usr/local/bin printf '%s' "/usr/local/bin" } check_bin_dir() { _cbd_bin_dir="$1" if [ ! -d "$_cbd_bin_dir" ]; then error "Installation directory ${_cbd_bin_dir} does not exist." info "Create it first or choose a different directory with --bin-dir." return 1 fi _cbd_good="" _cbd_old_ifs="${IFS:-}" IFS=: for _cbd_path in $PATH; do if [ "${_cbd_path}" = "${_cbd_bin_dir}" ]; then _cbd_good=1 break fi done IFS="$_cbd_old_ifs" if [ "${_cbd_good}" != "1" ]; then warn "${_cbd_bin_dir} is not in your \$PATH. Add it with:" info " export PATH=\"${_cbd_bin_dir}:\$PATH\"" fi } elevate_priv() { if ! has sudo; then error "Could not find \"sudo\", needed to install to ${BIN_DIR}." info "Run this script as root, or use --bin-dir to specify a writable directory." return 1 fi if ! sudo -v; then error "Superuser not granted, aborting installation." return 1 fi } download() { _dl_file="$1" _dl_url="$2" touch "$_dl_file" if has curl; then _dl_cmd="curl --fail --silent --location ${CURL_RETRY_OPTS} --output $_dl_file $_dl_url" elif has wget; then _dl_cmd="wget --quiet --output-document=$_dl_file $_dl_url" else error "No HTTP download program (curl, wget) found." return 1 fi debug "Download command: ${_dl_cmd}" $_dl_cmd && return 0 || _dl_rc=$? error "Download failed (exit code ${_dl_rc})." info "URL: ${BLUE}${_dl_url}${NO_COLOR}" info "The requested version or branch may not exist, or the build may not have completed." return "$_dl_rc" } confirm() { if [ -t 0 ]; then if [ -z "${FORCE:-}" ]; then printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[y/N]${NO_COLOR}" set +e read -r _cf_yn