#!/usr/bin/env sh # dcd installer for macOS and Linux. # # Usage: # curl -fsSL https://get.devicecloud.dev/install.sh | sh # # Env vars: # DCD_VERSION Pin a specific version (default: latest) # DCD_INSTALL_DIR Override install location (default: $HOME/.dcd/bin) # DCD_DOWNLOAD_BASE Override the download host (default: https://get.devicecloud.dev) # # The whole script is wrapped in main() and only invoked on the last line, so # a truncated download (curl | sh executes as it streams) runs nothing at all. set -eu err() { printf 'error: %s\n' "$1" >&2 exit 1 } info() { printf '%s\n' "$1" } # Find a dcd on PATH other than the one we just installed — usually a leftover # `npm install -g @devicecloud.dev/dcd` that can shadow this binary. Runs in a # subshell so the temporary IFS change never leaks back to the caller. find_conflicting_dcd() { ( IFS=: for dir in $PATH; do [ -n "$dir" ] || continue if [ "$dir" != "$INSTALL_DIR" ] && [ -x "$dir/dcd" ]; then printf '%s\n' "$dir/dcd" exit 0 fi done exit 1 ) } # Pick the shell rc file to persist PATH into, based on the login shell. rc_file() { case "${SHELL:-}" in *zsh) printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc" ;; *bash) if [ -f "$HOME/.bashrc" ]; then printf '%s\n' "$HOME/.bashrc" else printf '%s\n' "$HOME/.bash_profile" fi ;; *) printf '%s\n' "$HOME/.profile" ;; esac } main() { DOWNLOAD_BASE="${DCD_DOWNLOAD_BASE:-https://get.devicecloud.dev}" INSTALL_DIR="${DCD_INSTALL_DIR:-$HOME/.dcd/bin}" # --- detect platform --- os=$(uname -s) case "$os" in Darwin) os_id=darwin ;; Linux) os_id=linux ;; *) err "Unsupported OS: $os. Try the Windows installer (install.ps1)." ;; esac arch=$(uname -m) case "$arch" in arm64|aarch64) arch_id=arm64 ;; x86_64|amd64) arch_id=x64 ;; *) err "Unsupported architecture: $arch" ;; esac asset="dcd-${os_id}-${arch_id}" # --- resolve version --- if [ -n "${DCD_VERSION:-}" ]; then version="$DCD_VERSION" else info "Resolving latest version..." # /latest.json returns { "version": "5.1.0", ... } version=$( curl -fsSL "$DOWNLOAD_BASE/latest.json" \ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -n1 ) [ -z "$version" ] && err "Could not resolve latest version from $DOWNLOAD_BASE/latest.json" fi url="$DOWNLOAD_BASE/download/${version}/${asset}" sums_url="$DOWNLOAD_BASE/download/${version}/SHA256SUMS" info "Installing dcd ${version} (${os_id}-${arch_id})" info " from: $url" info " to: $INSTALL_DIR/dcd" # --- download --- mkdir -p "$INSTALL_DIR" tmp=$(mktemp "${TMPDIR:-/tmp}/dcd-XXXXXX") trap 'rm -f "$tmp" "$tmp.sums"' EXIT curl -fSL --progress-bar "$url" -o "$tmp" \ || err "Download failed: $url" # --- verify checksum --- curl -fsSL "$sums_url" -o "$tmp.sums" \ || err "Could not fetch checksums: $sums_url" expected=$(grep -F " ${asset}" "$tmp.sums" | awk '{print $1}' | head -n1) [ -z "$expected" ] && err "SHA256SUMS has no entry for $asset" if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$tmp" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$tmp" | awk '{print $1}') else err "Need sha256sum or shasum to verify download" fi if [ "$expected" != "$actual" ]; then err "Checksum mismatch for $asset: expected $expected, got $actual" fi # --- install --- chmod +x "$tmp" mv "$tmp" "$INSTALL_DIR/dcd" trap - EXIT # tmp has been moved; nothing to clean up # --- PATH setup --- path_line="export PATH=\"$INSTALL_DIR:\$PATH\"" case ":$PATH:" in *":$INSTALL_DIR:"*) on_path=1 ;; *) on_path=0 ;; esac info "" info "✓ Installed dcd $version to $INSTALL_DIR/dcd" if [ "$on_path" -eq 0 ]; then rc=$(rc_file) if [ -f "$rc" ] && grep -Fq "$INSTALL_DIR" "$rc" 2>/dev/null; then # rc already references the dir (e.g. a re-install); don't duplicate it. info "" info " $INSTALL_DIR is already configured in $rc." info " Restart your shell, or run: $path_line" elif printf '\n# dcd\n%s\n' "$path_line" >> "$rc" 2>/dev/null; then info "" info " Added $INSTALL_DIR to your PATH in $rc." info " Restart your shell, or run: $path_line" else info "" info " $INSTALL_DIR is not on your PATH. Add this to your shell rc:" info " $path_line" fi fi # --- warn about a conflicting (shadowing) install --- if conflict=$(find_conflicting_dcd); then info "" info "! Another dcd is already on your PATH:" info " $conflict" info " This is usually a previous 'npm install -g @devicecloud.dev/dcd', which" info " can shadow this binary depending on PATH order. Remove it with:" info " npm uninstall -g @devicecloud.dev/dcd" fi if [ "$on_path" -eq 1 ]; then info "" info " Try: dcd --help" fi } main "$@"