diff --git a/README.md b/README.md index b998f52c..f9c62461 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ brew install livekit-cli curl -sSL https://get.livekit.io/cli | bash ``` +The script installs `lk` to `/usr/local/bin`. Set `LK_INSTALL_PATH` to install somewhere else. + Or download a precompiled binary for the [latest release](https://github.com/livekit/livekit-cli/releases/latest) ## Windows diff --git a/cmd/lk/banner.go b/cmd/lk/banner.go index af561cc1..017f809b 100644 --- a/cmd/lk/banner.go +++ b/cmd/lk/banner.go @@ -27,6 +27,7 @@ import ( "charm.land/lipgloss/v2" "github.com/Masterminds/semver/v3" + "github.com/urfave/cli/v3" livekitcli "github.com/livekit/livekit-cli/v2" "github.com/livekit/livekit-cli/v2/pkg/util" @@ -133,10 +134,12 @@ func bannerMessages(raw []byte, version string) []string { // showBanner prints the notices before the command runs. It blocks so the // banner owns the terminal first: prompts and spinners write to the terminal -// directly and cannot be held behind it. Non-interactive runs (scripts, pipes) -// skip the fetch entirely. -func showBanner(ctx context.Context) { - if !out.Interactive() { +// directly and cannot be held behind it. Runs that a script or tool drives +// (stderr or stdin isn't a terminal, or --yes) skip the fetch entirely: the +// install script and coding agents invoke lk that way, and a notice there is +// noise. A person at a terminal sees it on their next command. +func showBanner(ctx context.Context, cmd *cli.Command) { + if !out.Interactive() || SkipPrompts(cmd) { return } printBanner(loadBanner(ctx), out.Err) diff --git a/cmd/lk/main.go b/cmd/lk/main.go index 395d553a..9cccc38a 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -192,7 +192,7 @@ func initLogger(ctx context.Context, cmd *cli.Command) (context.Context, error) } util.DetectBackground() - showBanner(ctx) + showBanner(ctx, cmd) // Nudge users still on API-key auth toward the new account-based flow. maybeShowUpgradeNotice(cmd, conf) diff --git a/install-cli.sh b/install-cli.sh index 260fac8c..9059b39c 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -14,7 +14,12 @@ # limitations under the License. # LiveKit CLI installer for Linux. Downloads the latest GitHub release archive -# and installs the binary + completions system-wide. +# and installs the binary + completions system-wide. On macOS, use Homebrew. +# +# Environment: +# LK_INSTALL_PATH where to put lk (default /usr/local/bin) +# LK_INSTALL_MAX_ATTEMPTS download attempts (default 5) +# LK_INSTALL_RETRY_DELAY seconds between attempts (default 2) set -u set -o errtrace @@ -23,12 +28,14 @@ set -o pipefail REPO="livekit-cli" BIN_NAME="lk" -INSTALL_PATH="/usr/local/bin" +INSTALL_PATH="${LK_INSTALL_PATH:-/usr/local/bin}" +INSTALL_PATH="${INSTALL_PATH%/}" BASH_COMPLETION_PATH="/usr/share/bash-completion/completions" ZSH_COMPLETION_PATH="/usr/share/zsh/site-functions" FISH_COMPLETION_PATH="/usr/share/fish/vendor_completions.d" log() { printf "%b\n" "$*"; } +warn() { printf "%b\n" "$*" >&2; } abort() { printf "%s\n" "$@" >&2; exit 1; } @@ -41,16 +48,14 @@ RETRY_DELAY="${LK_INSTALL_RETRY_DELAY:-2}" [[ "$RETRY_DELAY" =~ ^[0-9]+$ ]] && [ "$RETRY_DELAY" -ge 1 ] || abort "LK_INSTALL_RETRY_DELAY must be a positive integer (seconds)" [ -n "${BASH_VERSION:-}" ] || abort "This script requires bash" -[ -d "$INSTALL_PATH" ] || abort "Could not install, $INSTALL_PATH doesn't exist" command -v curl >/dev/null || abort "cURL is required and is not found" command -v sha256sum >/dev/null || abort "sha256sum is required and is not found" -command -v jq >/dev/null || abort "jq is required and is not found" OS="$(uname)" case "$OS" in - Darwin) abort "Installer not supported on MacOS, please install using Homebrew." ;; + Darwin) abort "Installer not supported on macOS. Install with Homebrew: brew install livekit-cli" ;; Linux) ;; - *) abort "Installer is only supported on Linux." ;; + *) abort "Installer is only supported on Linux. On Windows, run: winget install LiveKit.LiveKitCLI" ;; esac case "$(uname -m)" in @@ -59,14 +64,33 @@ case "$(uname -m)" in *) abort "Unsupported architecture: $(uname -m)" ;; esac +# Use sudo only for a directory this user can't write to (or create). SUDO_PREFIX="" -if [ ! -w "$INSTALL_PATH" ]; then - SUDO_PREFIX="sudo" +if [ -d "$INSTALL_PATH" ]; then + [ -w "$INSTALL_PATH" ] || SUDO_PREFIX="sudo" +elif [ -n "${LK_INSTALL_PATH:-}" ]; then + parent="$(dirname "$INSTALL_PATH")" + while [ ! -d "$parent" ]; do parent="$(dirname "$parent")"; done + [ -w "$parent" ] || SUDO_PREFIX="sudo" +else + abort "Could not install, $INSTALL_PATH doesn't exist" +fi +if [ -n "$SUDO_PREFIX" ]; then log "sudo is required to install to $INSTALL_PATH" fi -VERSION=$(curl -fsSL --retry "$MAX_ATTEMPTS" --retry-delay "$RETRY_DELAY" https://api.github.com/repos/livekit/$REPO/releases/latest \ - | jq -r '.tag_name' | sed 's/^v//') +# This script never makes lk itself a symlink (only livekit-cli), so one here is +# another package manager's, such as Homebrew's. Replacing it would leave that +# manager thinking its copy is still installed. +if [ -L "$INSTALL_PATH/$BIN_NAME" ]; then + abort "$INSTALL_PATH/$BIN_NAME is a symlink, probably Homebrew's. Run \`brew uninstall livekit-cli\` first, or set LK_INSTALL_PATH to install elsewhere." +fi + +# The latest release's tag, from where /releases/latest redirects. This needs +# no API token or jq. +LATEST_URL=$(curl -fsSLI -o /dev/null -w '%{url_effective}' --retry "$MAX_ATTEMPTS" --retry-delay "$RETRY_DELAY" \ + "https://github.com/livekit/$REPO/releases/latest") +VERSION="${LATEST_URL##*/v}" [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || abort "Invalid version: $VERSION" @@ -96,12 +120,14 @@ actual_sum=$(sha256sum "$TEMP_DIR/$ARCHIVE_NAME" | awk '{print $1}') tar xzf "$TEMP_DIR/$ARCHIVE_NAME" -C "$TEMP_DIR" +$SUDO_PREFIX mkdir -p "$INSTALL_PATH" $SUDO_PREFIX mv "$TEMP_DIR/$BIN_NAME" "$INSTALL_PATH/$BIN_NAME" $SUDO_PREFIX ln -sf "$INSTALL_PATH/$BIN_NAME" "$INSTALL_PATH/livekit-cli" # Install completions if the corresponding system directories exist. The fish # completion ships in the archive (no need to invoke lk to regenerate it). -if [ -d "$TEMP_DIR/autocomplete" ]; then +# Skipped for a custom LK_INSTALL_PATH, which is usually a user-level install. +if [ -d "$TEMP_DIR/autocomplete" ] && [ -z "${LK_INSTALL_PATH:-}" ]; then [ -d "$BASH_COMPLETION_PATH" ] && \ $SUDO_PREFIX install -m 0644 "$TEMP_DIR/autocomplete/bash_autocomplete" "$BASH_COMPLETION_PATH/livekit-cli" || true [ -d "$ZSH_COMPLETION_PATH" ] && \ @@ -111,3 +137,12 @@ if [ -d "$TEMP_DIR/autocomplete" ]; then fi log "\n$BIN_NAME is installed to $INSTALL_PATH\n" + +# Another lk earlier on PATH (often Homebrew's) would shadow this one. +found="$(command -v "$BIN_NAME" 2>/dev/null || true)" +if [ -z "$found" ]; then + warn "Note: $INSTALL_PATH isn't on your PATH. Add it to run \`$BIN_NAME\` by name.\n" +elif [ "$found" != "$INSTALL_PATH/$BIN_NAME" ]; then + warn "Note: $found comes first on your PATH, so \`$BIN_NAME\` runs that copy, not this one." + warn "Remove it (for Homebrew: brew uninstall livekit-cli) or put $INSTALL_PATH earlier on PATH.\n" +fi