From 8547614b7298b099f7134e19d5578a897fe6ffa3 Mon Sep 17 00:00:00 2001 From: Jack Dwyer Date: Mon, 19 Oct 2026 15:21:59 -0700 Subject: [PATCH 1/5] feat(install): support macOS and install skills in the curl installer install-cli.sh (served at get.livekit.io/cli) now installs on macOS as well as Linux, picking the darwin archive for Intel or Apple silicon. It no longer needs jq or sha256sum, which older macOS lacks, and creates /usr/local/bin when a fresh Mac doesn't have it. LK_INSTALL_PATH installs elsewhere. After installing, it runs `lk skills install --global --yes` when someone is watching (stdout is a terminal) or LK_INSTALL_SKILLS=1, and skips it in CI, as root, or with LK_INSTALL_SKILLS=0. A failure there doesn't fail the install. It also warns when another lk, such as Homebrew's, comes first on PATH. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++- install-cli.sh | 110 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b998f52cd..6fc349bec 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,16 @@ This package includes command line utilities that interacts with LiveKit. It all brew install livekit-cli ``` -## Linux +Or use the install script below. + +## Linux and Mac ```shell curl -sSL https://get.livekit.io/cli | bash ``` +The script installs `lk` to `/usr/local/bin` (set `LK_INSTALL_PATH` to change it), then installs LiveKit's [coding agent skills](#coding-agent-skills) and Docs MCP server for the coding agents it finds. Set `LK_INSTALL_SKILLS=0` to skip that step, or `LK_INSTALL_SKILLS=1` to run it without a terminal. + Or download a precompiled binary for the [latest release](https://github.com/livekit/livekit-cli/releases/latest) ## Windows diff --git a/install-cli.sh b/install-cli.sh index 260fac8c7..9ff62a8d3 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -13,8 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -# LiveKit CLI installer for Linux. Downloads the latest GitHub release archive -# and installs the binary + completions system-wide. +# LiveKit CLI installer for Linux and macOS. Downloads the latest GitHub +# release archive, installs the binary + completions system-wide, then installs +# LiveKit's skills and Docs MCP server into the coding agents on this machine. +# +# Environment: +# LK_INSTALL_PATH where to put lk (default /usr/local/bin) +# LK_INSTALL_SKILLS 1 installs skills even without a terminal; 0 skips them +# LK_INSTALL_MAX_ATTEMPTS download attempts (default 5) +# LK_INSTALL_RETRY_DELAY seconds between attempts (default 2) set -u set -o errtrace @@ -23,12 +30,10 @@ set -o pipefail REPO="livekit-cli" BIN_NAME="lk" -INSTALL_PATH="/usr/local/bin" -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" +INSTALL_PATH="${LK_INSTALL_PATH:-/usr/local/bin}" log() { printf "%b\n" "$*"; } +warn() { printf "%b\n" "$*" >&2; } abort() { printf "%s\n" "$@" >&2; exit 1; } @@ -41,36 +46,66 @@ 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." ;; - Linux) ;; - *) abort "Installer is only supported on Linux." ;; + Darwin) + OS_NAME="darwin" + # /usr/local/bin is on macOS's default PATH (/etc/paths), but a fresh Apple + # silicon Mac may not have it yet; it's created below. + BASH_COMPLETION_PATH="/usr/local/etc/bash_completion.d" + ZSH_COMPLETION_PATH="/usr/local/share/zsh/site-functions" + FISH_COMPLETION_PATH="/usr/local/share/fish/vendor_completions.d" + ;; + Linux) + OS_NAME="linux" + 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" + ;; + *) abort "Installer is only supported on Linux and macOS. On Windows, run: winget install LiveKit.LiveKitCLI" ;; esac case "$(uname -m)" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) abort "Unsupported architecture: $(uname -m)" ;; + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) abort "Unsupported architecture: $(uname -m)" ;; esac +# macOS before 15 has shasum but not sha256sum. +if command -v sha256sum >/dev/null; then + sha256() { sha256sum "$1" | awk '{print $1}'; } +elif command -v shasum >/dev/null; then + sha256() { shasum -a 256 "$1" | awk '{print $1}'; } +else + abort "sha256sum or shasum is required and neither was found" +fi + +# 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 [ "$OS_NAME" = "darwin" ] || [ -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//') +# The latest release's tag, from where /releases/latest redirects. This needs +# no API token or jq, which macOS before 15 doesn't ship. +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" -ARCHIVE_NAME="${BIN_NAME}_${VERSION}_linux_${ARCH}.tar.gz" +ARCHIVE_NAME="${BIN_NAME}_${VERSION}_${OS_NAME}_${ARCH}.tar.gz" ARCHIVE_URL="https://github.com/livekit/$REPO/releases/download/v${VERSION}/${ARCHIVE_NAME}" CHECKSUMS_URL="https://github.com/livekit/$REPO/releases/download/v${VERSION}/checksums.txt" @@ -90,18 +125,20 @@ curl -fsSL --retry "$MAX_ATTEMPTS" --retry-delay "$RETRY_DELAY" "$CHECKSUMS_URL" log "Verifying checksum..." expected_sum=$(awk -v f="$ARCHIVE_NAME" '$2 == f {print $1}' "$TEMP_DIR/checksums.txt") [ -n "$expected_sum" ] || abort "Could not find a checksum for $ARCHIVE_NAME in checksums.txt" -actual_sum=$(sha256sum "$TEMP_DIR/$ARCHIVE_NAME" | awk '{print $1}') +actual_sum=$(sha256 "$TEMP_DIR/$ARCHIVE_NAME") [ "$expected_sum" = "$actual_sum" ] || \ abort "Checksum verification failed for $ARCHIVE_NAME (expected $expected_sum, got $actual_sum)" 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 +148,32 @@ 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 + +# Install LiveKit's skills and Docs MCP server into the coding agents on this +# machine, so they know how to build LiveKit agents. Runs when someone is +# watching (stdout is a terminal, since stdin is the script under curl | bash) +# or LK_INSTALL_SKILLS=1; never in CI, as root, or with LK_INSTALL_SKILLS=0. +# A failure here doesn't fail the install. +install_skills="${LK_INSTALL_SKILLS:-}" +if [ -z "$install_skills" ]; then + if [ -t 1 ] && [ -z "${CI:-}" ]; then install_skills=1; else install_skills=0; fi +fi +if [ "$install_skills" = "1" ]; then + if [ "$(id -u)" = "0" ]; then + log "Skipping LiveKit skills when installing as root; as your own user, run: lk skills install --global" + else + log "Installing LiveKit skills for your coding agents (LK_INSTALL_SKILLS=0 skips this)..." + if ! "$INSTALL_PATH/$BIN_NAME" skills install --global --yes; then + warn "Couldn't install LiveKit skills. Run \`lk skills install --global\` to try again." + fi + fi +fi From ca82969d298860ada481f9b05707b742b4d3eb95 Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Wed, 21 Oct 2026 15:37:48 -0400 Subject: [PATCH 2/5] fix(install): strip a trailing slash from LK_INSTALL_PATH With LK_INSTALL_PATH=.../bin/, the PATH check compared .../bin//lk to the .../bin/lk that command -v reports and warned that lk shadows itself. --- install-cli.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install-cli.sh b/install-cli.sh index 9ff62a8d3..a92b3b511 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -31,6 +31,7 @@ set -o pipefail REPO="livekit-cli" BIN_NAME="lk" INSTALL_PATH="${LK_INSTALL_PATH:-/usr/local/bin}" +INSTALL_PATH="${INSTALL_PATH%/}" log() { printf "%b\n" "$*"; } warn() { printf "%b\n" "$*" >&2; } From d5996a7ef846f8381a96d0ed773863f3ac9543ed Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Wed, 21 Oct 2026 15:37:48 -0400 Subject: [PATCH 3/5] fix(banner): skip the notice on scripted runs showBanner only checked that stderr is a terminal, so the install script's `lk skills install` step printed the banner mid-install: stderr is the user's terminal there even though stdin is the script. Coding agents that call lk hit the same thing. Gate it on SkipPrompts too (stdin isn't a terminal, or --yes), the signal the prompts already use. A person at a terminal still sees it on their next command. --- cmd/lk/banner.go | 11 +++++++---- cmd/lk/main.go | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/lk/banner.go b/cmd/lk/banner.go index af561cc16..017f809b0 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 395d553a5..9cccc38a3 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) From a02aaa8bc923da25be1e348d168eb2e250941fc4 Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Wed, 21 Oct 2026 15:37:49 -0400 Subject: [PATCH 4/5] fix(install): don't suggest retrying skills install when no coding agent is found When lk detects no coding agent, the script told the user to run `lk skills install --global` to try again, which can't succeed. Say to run it once a coding agent is installed, which also covers a transient failure. --- install-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-cli.sh b/install-cli.sh index a92b3b511..a900ece36 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -174,7 +174,7 @@ if [ "$install_skills" = "1" ]; then else log "Installing LiveKit skills for your coding agents (LK_INSTALL_SKILLS=0 skips this)..." if ! "$INSTALL_PATH/$BIN_NAME" skills install --global --yes; then - warn "Couldn't install LiveKit skills. Run \`lk skills install --global\` to try again." + warn "Couldn't install LiveKit skills. With a coding agent (Claude Code, Codex, Cursor, ...) installed, run: lk skills install --global" fi fi fi From de65d4998f55b5219b15d0b0574a7dc4775495e1 Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Wed, 21 Oct 2026 15:47:24 -0400 Subject: [PATCH 5/5] fix(install): refuse to replace a symlinked lk, such as Homebrew's On Intel Macs Homebrew's prefix is /usr/local, so brew install livekit-cli puts a symlink at /usr/local/bin/lk, the script's default target. The mv replaced it with the binary silently: the PATH warning can't fire because command -v lk matches the install path, and the directory is user-writable so there's no sudo prompt. brew upgrade then fails with "Target already exists". This script only ever makes livekit-cli a symlink, never lk, so a symlink there is another package manager's: stop before the download and say to brew uninstall or set LK_INSTALL_PATH. --- install-cli.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/install-cli.sh b/install-cli.sh index a900ece36..0754be4e0 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -98,6 +98,14 @@ if [ -n "$SUDO_PREFIX" ]; then log "sudo is required to install to $INSTALL_PATH" fi +# Homebrew links lk into its prefix's bin, which on Intel Macs is /usr/local/bin. +# Replacing that symlink would leave brew thinking its copy is still installed. +# This script never makes lk itself a symlink (only livekit-cli), so one here is +# another package manager's. +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, which macOS before 15 doesn't ship. LATEST_URL=$(curl -fsSLI -o /dev/null -w '%{url_effective}' --retry "$MAX_ATTEMPTS" --retry-delay "$RETRY_DELAY" \