Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions cmd/lk/banner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/lk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
119 changes: 97 additions & 22 deletions install-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,12 +30,11 @@ 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}"
INSTALL_PATH="${INSTALL_PATH%/}"

log() { printf "%b\n" "$*"; }
warn() { printf "%b\n" "$*" >&2; }
abort() { printf "%s\n" "$@" >&2; exit 1; }


Expand All @@ -41,36 +47,74 @@ 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//')
# 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" \
"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"

Expand All @@ -90,18 +134,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" ] && \
Expand All @@ -111,3 +157,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. With a coding agent (Claude Code, Codex, Cursor, ...) installed, run: lk skills install --global"
fi
fi
fi
Loading