diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..1829dd9 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,317 @@ +#!/bin/sh +# Copyright (c) Pinecone Systems, Inc. +# +# Install script for the Pinecone CLI (pc). +# +# This script detects your operating system and architecture, downloads the +# latest release of the Pinecone CLI from GitHub, verifies the checksum, and +# installs it to /usr/local/bin (or a directory of your choice). +# +# Usage: +# curl -fsSL https://pinecone.io/install.sh | sh +# +# Environment variables: +# PINECONE_VERSION Pin to a specific version (e.g. "0.4.2"). Default: latest. +# PINECONE_INSTALL Installation directory. Default: /usr/local/bin. +# PINECONE_NO_VERIFY Set to 1 to skip checksum verification. + +set -eu + +GITHUB_REPO="pinecone-io/cli" +BINARY_NAME="pc" + +# All the code is wrapped in a main function that gets called at the +# bottom of the file, so that a truncated partial download doesn't end +# up executing half a script. +main() { + INSTALL_DIR="${PINECONE_INSTALL:-/usr/local/bin}" + VERSION="${PINECONE_VERSION:-}" + VERSION="${VERSION#v}" + NO_VERIFY="${PINECONE_NO_VERIFY:-0}" + + need_cmd uname + + # ------------------------------------------------------- + # Step 1: Detect OS and architecture + # ------------------------------------------------------- + OS="" + ARCH="" + FILENAME="" + + detect_platform + + # ------------------------------------------------------- + # Step 2: Find an HTTP download tool (curl or wget) + # ------------------------------------------------------- + HTTP="" + if command_exists curl; then + HTTP="curl" + elif command_exists wget; then + HTTP="wget" + else + err "Either curl or wget is required to download files. Please install one and try again." + fi + + # ------------------------------------------------------- + # Step 3: Resolve version + # ------------------------------------------------------- + if [ -z "$VERSION" ]; then + log "Fetching latest release version..." + VERSION=$(get_latest_version) + if [ -z "$VERSION" ]; then + err "Could not determine latest version. Set PINECONE_VERSION and try again." + fi + fi + + log "Installing Pinecone CLI v${VERSION} (${OS}/${ARCH})" + + # ------------------------------------------------------- + # Step 4: Build download URLs + # ------------------------------------------------------- + BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}" + ARCHIVE_URL="${BASE_URL}/${FILENAME}" + CHECKSUMS_URL="${BASE_URL}/pc_${VERSION}_checksums.txt" + + # ------------------------------------------------------- + # Step 5: Download and extract to a temp directory + # ------------------------------------------------------- + TMPDIR_ROOT="${TMPDIR:-/tmp}" + WORK_DIR=$(mktemp -d "${TMPDIR_ROOT}/pinecone-cli.XXXXXX") + trap 'rm -rf "$WORK_DIR"' EXIT + + log "Downloading ${ARCHIVE_URL}..." + download "$ARCHIVE_URL" "${WORK_DIR}/${FILENAME}" + + # ------------------------------------------------------- + # Step 6: Verify checksum (unless opted out) + # ------------------------------------------------------- + if [ "$NO_VERIFY" != "1" ]; then + verify_checksum + else + log "Skipping checksum verification (PINECONE_NO_VERIFY=1)" + fi + + # ------------------------------------------------------- + # Step 7: Extract the binary + # ------------------------------------------------------- + log "Extracting..." + tar -xzf "${WORK_DIR}/${FILENAME}" -C "$WORK_DIR" + + if [ ! -f "${WORK_DIR}/${BINARY_NAME}" ]; then + err "Archive did not contain expected binary '${BINARY_NAME}'. Contents of archive:" \ + "$(ls -la "$WORK_DIR")" + fi + + chmod +x "${WORK_DIR}/${BINARY_NAME}" + + # ------------------------------------------------------- + # Step 8: Install the binary + # ------------------------------------------------------- + install_binary + + # ------------------------------------------------------- + # Done + # ------------------------------------------------------- + log "" + log "Pinecone CLI v${VERSION} installed successfully to ${INSTALL_DIR}/${BINARY_NAME}" + log "" + log "Run 'pc --help' to get started." +} + +# ========================================================= +# Helpers +# ========================================================= + +log() { + printf '%s\n' "$@" +} + +err() { + printf 'Error: %s\n' "$@" >&2 + exit 1 +} + +need_cmd() { + if ! command_exists "$1"; then + err "'$1' is required but not found on your system." + fi +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# ========================================================= +# Platform detection +# ========================================================= + +detect_platform() { + local uname_os uname_arch + + uname_os="$(uname -s)" + uname_arch="$(uname -m)" + + case "$uname_os" in + Darwin) + OS="Darwin" + ARCH="all" + FILENAME="pc_Darwin_all.tar.gz" + ;; + Linux) + OS="Linux" + case "$uname_arch" in + x86_64|amd64) + ARCH="x86_64" + FILENAME="pc_Linux_x86_64.tar.gz" + ;; + aarch64|arm64) + ARCH="arm64" + FILENAME="pc_Linux_arm64.tar.gz" + ;; + i386|i686) + ARCH="i386" + FILENAME="pc_Linux_i386.tar.gz" + ;; + *) + err "Unsupported Linux architecture: ${uname_arch}" \ + "Supported architectures: x86_64, arm64, i386" + ;; + esac + ;; + *) + err "Unsupported operating system: ${uname_os}" \ + "This installer supports macOS (Darwin) and Linux." \ + "For Windows, download the .zip from https://github.com/${GITHUB_REPO}/releases" + ;; + esac +} + +# ========================================================= +# HTTP helpers +# ========================================================= + +download() { + local url="$1" + local dest="$2" + + case "$HTTP" in + curl) + curl -fSL --progress-bar -o "$dest" "$url" || err "Failed to download: ${url}" + ;; + wget) + wget -q --show-progress -O "$dest" "$url" || err "Failed to download: ${url}" + ;; + esac +} + +fetch() { + # Fetch URL contents to stdout (silent) + local url="$1" + case "$HTTP" in + curl) curl -fsSL "$url" ;; + wget) wget -qO- "$url" ;; + esac +} + +get_latest_version() { + # Use the GitHub API to get the latest release tag. + # Falls back to following the /releases/latest redirect if the API is rate-limited. + local tag + + tag=$(fetch "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null \ + | grep '"tag_name"' \ + | sed -E 's/.*"tag_name":[[:space:]]*"v?([^"]+)".*/\1/' ) || true + + if [ -z "$tag" ] && [ "$HTTP" = "curl" ]; then + # Fallback: follow the redirect and parse the URL + tag=$(curl -fsSL -o /dev/null -w '%{url_effective}' \ + "https://github.com/${GITHUB_REPO}/releases/latest" 2>/dev/null \ + | sed 's|.*/v||') || true + fi + + printf '%s' "$tag" +} + +# ========================================================= +# Checksum verification +# ========================================================= + +verify_checksum() { + local sha_cmd="" + + if command_exists sha256sum; then + sha_cmd="sha256sum" + elif command_exists shasum; then + sha_cmd="shasum -a 256" + else + log "Warning: Neither sha256sum nor shasum found. Skipping checksum verification." + return 0 + fi + + log "Verifying checksum..." + download "$CHECKSUMS_URL" "${WORK_DIR}/checksums.txt" + + local expected actual + expected=$(awk -v f="${FILENAME}" '$2 == f {print $1}' "${WORK_DIR}/checksums.txt") + + if [ -z "$expected" ]; then + err "Could not find checksum for ${FILENAME} in checksums file." + fi + + actual=$($sha_cmd "${WORK_DIR}/${FILENAME}" | awk '{print $1}') + + if [ "$expected" != "$actual" ]; then + err "Checksum verification failed!" \ + "Expected: ${expected}" \ + "Actual: ${actual}" \ + "The downloaded file may be corrupted. Please try again." + fi + + log "Checksum verified." +} + +# ========================================================= +# Installation +# ========================================================= + +install_binary() { + # Try unprivileged directory creation first so a user-owned path like + # $HOME/.local/bin is never created as root via sudo. + if [ ! -d "$INSTALL_DIR" ]; then + mkdir -p "$INSTALL_DIR" 2>/dev/null || true + fi + + # Create install directory if it doesn't exist, then move binary into it. + if [ -d "$INSTALL_DIR" ] && [ -w "$INSTALL_DIR" ]; then + mv "${WORK_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" + elif command_exists sudo; then + log "Password may be required to install to ${INSTALL_DIR}." + sudo sh -c "mkdir -p \"$INSTALL_DIR\" && mv \"${WORK_DIR}/${BINARY_NAME}\" \"${INSTALL_DIR}/${BINARY_NAME}\"" + elif command_exists doas; then + log "Password may be required to install to ${INSTALL_DIR}." + doas sh -c "mkdir -p \"$INSTALL_DIR\" && mv \"${WORK_DIR}/${BINARY_NAME}\" \"${INSTALL_DIR}/${BINARY_NAME}\"" + else + err "Cannot write to ${INSTALL_DIR} and neither sudo nor doas are available." \ + "Either run this script as root or set PINECONE_INSTALL to a writable directory:" \ + " curl -fsSL https://pinecone.io/install.sh | PINECONE_INSTALL=\$HOME/.local/bin sh" + fi + + # Ensure the install directory is in PATH + case ":${PATH}:" in + *":${INSTALL_DIR}:"*) ;; + *) + log "" + log "NOTE: ${INSTALL_DIR} is not in your \$PATH." + log "Add it by running one of the following:" + log "" + log " # For bash" + log " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" + log "" + log " # For zsh" + log " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" + log "" + ;; + esac +} + +main diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..9a44806 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,155 @@ +#!/bin/sh +# Copyright (c) Pinecone Systems, Inc. +# +# Uninstall script for the Pinecone CLI (pc). +# +# This script removes the Pinecone CLI binary and optionally removes +# configuration and data files created by the CLI. +# +# Usage: +# curl -fsSL https://pinecone.io/uninstall.sh | sh +# +# Or, if you have the script locally: +# cat uninstall.sh | sh +# +# Environment variables: +# PINECONE_INSTALL Installation directory where pc was installed. Default: /usr/local/bin. +# PINECONE_KEEP_CONFIG Set to 1 to keep configuration files. Default: remove them. + +set -eu + +BINARY_NAME="pc" + +main() { + INSTALL_DIR="${PINECONE_INSTALL:-/usr/local/bin}" + KEEP_CONFIG="${PINECONE_KEEP_CONFIG:-0}" + + BINARY_PATH="${INSTALL_DIR}/${BINARY_NAME}" + + # ------------------------------------------------------- + # Step 0: Check for package-manager installations + # ------------------------------------------------------- + check_package_manager + + # ------------------------------------------------------- + # Step 1: Remove the binary + # ------------------------------------------------------- + remove_binary + + # ------------------------------------------------------- + # Step 2: Remove configuration and data files + # ------------------------------------------------------- + if [ "$KEEP_CONFIG" != "1" ]; then + remove_config + else + log "Keeping configuration files (PINECONE_KEEP_CONFIG=1)." + fi + + # ------------------------------------------------------- + # Done + # ------------------------------------------------------- + log "" + log "Pinecone CLI has been uninstalled." +} + +# ========================================================= +# Helpers +# ========================================================= + +log() { + printf '%s\n' "$@" +} + +err() { + printf 'Error: %s\n' "$@" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# ========================================================= +# Package-manager detection +# ========================================================= + +check_package_manager() { + # Homebrew: check if pc resolves into a Homebrew prefix (Cellar or Caskroom) + if [ -e "$BINARY_PATH" ] && command_exists brew; then + BREW_PREFIX="$(brew --prefix 2>/dev/null || true)" + REAL_PC="$(readlink "$BINARY_PATH" 2>/dev/null || true)" + # readlink may return a relative path on macOS; resolve it to absolute + case "$REAL_PC" in + /*) ;; + ?*) REAL_PC="$(cd "$(dirname "$BINARY_PATH")" && cd "$(dirname "$REAL_PC")" && pwd)/$(basename "$REAL_PC")" ;; + esac + case "$REAL_PC" in + "${BREW_PREFIX}"/Caskroom/*) + CASK_NAME="$(echo "$REAL_PC" | sed "s|${BREW_PREFIX}/Caskroom/||" | cut -d/ -f1)" + log "Pinecone CLI appears to have been installed via Homebrew (cask: ${CASK_NAME})." + log "You can uninstall it with:" + log "" + log " brew uninstall --cask ${CASK_NAME}" + log "" + exit 0 + ;; + "${BREW_PREFIX}"/Cellar/*) + FORMULA_NAME="$(echo "$REAL_PC" | sed "s|${BREW_PREFIX}/Cellar/||" | cut -d/ -f1)" + log "Pinecone CLI appears to have been installed via Homebrew (formula: ${FORMULA_NAME})." + log "You can uninstall it with:" + log "" + log " brew uninstall ${FORMULA_NAME}" + log "" + exit 0 + ;; + esac + fi + +} + +# ========================================================= +# Binary removal +# ========================================================= + +remove_binary() { + if [ ! -f "$BINARY_PATH" ]; then + log "Binary not found at ${BINARY_PATH}. It may have already been removed." + return 0 + fi + + log "Removing ${BINARY_PATH}..." + + if [ -w "$INSTALL_DIR" ]; then + rm -f "$BINARY_PATH" + elif command_exists sudo; then + log "Password may be required to remove ${BINARY_PATH}." + sudo rm -f "$BINARY_PATH" + elif command_exists doas; then + log "Password may be required to remove ${BINARY_PATH}." + doas rm -f "$BINARY_PATH" + else + err "Cannot write to ${INSTALL_DIR} and neither sudo nor doas are available." \ + "Either run this script as root or set PINECONE_INSTALL to the directory where pc was installed:" \ + " curl -fsSL https://pinecone.io/uninstall.sh | PINECONE_INSTALL=\$HOME/.local/bin sh" + fi + + log "Binary removed." +} + +# ========================================================= +# Configuration removal +# ========================================================= + +remove_config() { + CONFIG_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/pinecone" + + if [ -d "$CONFIG_DIR" ]; then + log "Removing configuration directory ${CONFIG_DIR}..." + rm -rf "$CONFIG_DIR" + log "Configuration removed." + else + log "No configuration directory found." + fi +} + +main