diff --git a/README.md b/README.md index b998f52c..6fc349be 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/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..0754be4e 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,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; } @@ -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" @@ -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" ] && \ @@ -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