#!/bin/sh # The Lyriks bootstrap, served at https://get.lyriks.io. Run it as: # # curl -fsSL https://get.lyriks.io | sh -s -- community --license-key lyk_... # # Windows runs the sibling script from PowerShell instead (docs 22): # # irm https://get.lyriks.io/ps | iex # # Why this exists: the appliance kit is distributed as an OCI image, so Docker # must already work before the kit (and its own `./lyriks doctor`) can even # reach the host. Everything that must happen BEFORE that point lives here: # # architecture check -> OS check (WSL1 refused, Git Bash redirected to # install.ps1) -> Docker present or offered -> Compose v2 present or offered # -> kit pulled -> ./lyriks doctor -> ./lyriks install -> ./lyriks smoke # # Contract: everything is CHECKED silently, nothing is INSTALLED silently. # Every missing dependency is proposed and needs a yes (or --yes up front). # POSIX sh on purpose: `curl | sh` lands in dash on Debian/Ubuntu. Re-running # is safe: every step reconciles, exactly like `./lyriks install` itself. set -eu EDITION=community LICENSE_KEY="${LYRIKS_LICENSE_KEY:-}" HTTP_PORT="${LYRIKS_PORT:-3000}" ORIGIN="" ADMIN_EMAIL="" WORKSPACE_NAME="" REGISTRY_HOST="${LYRIKS_REGISTRY_HOST:-registry.lyriks.io}" LYRIKS_HOME="${LYRIKS_HOME:-$HOME/lyriks}" ASSUME_YES=0 DRY_RUN=0 FROM_WINDOWS=0 usage() { cat <<'EOF' Lyriks bootstrap curl -fsSL https://get.lyriks.io | sh -s -- [community|enterprise] [options] Options: --license-key Activate during the install (community may also paste the key later at /activate) --port Local HTTP port (default 3000) --origin Public origin for a server install (default http://localhost:) --admin-email REQUIRED (asked interactively if omitted): the operator account created at install; its temporary password prints once --workspace-name Name the first workspace --yes Assume consent for every proposed install --dry-run Print every state-changing command instead of running it EOF } while [ $# -gt 0 ]; do case "$1" in community|enterprise) EDITION="$1" ;; --license-key) shift; LICENSE_KEY="${1:-}" ;; --port) shift; HTTP_PORT="${1:-}" ;; --origin) shift; ORIGIN="${1:-}" ;; --admin-email) shift; ADMIN_EMAIL="${1:-}" ;; --workspace-name) shift; WORKSPACE_NAME="${1:-}" ;; --yes|-y) ASSUME_YES=1 ;; --dry-run) DRY_RUN=1 ;; # Internal, passed by install.ps1: the MCP client hint must come from the # OS the AI client runs on (Windows), not from inside WSL where this # script executes; the PowerShell side owns that step there. --from-windows) FROM_WINDOWS=1 ;; -h|--help) usage; exit 0 ;; *) printf 'Unknown option: %s\n\n' "$1" >&2; usage >&2; exit 2 ;; esac shift done say() { printf '\033[36m>\033[0m %s\n' "$1"; } ok() { printf '\033[32mOK\033[0m %s\n' "$1"; } die() { printf '\033[31mERROR: %s\033[0m\n' "$1" >&2; exit 1; } # Consent gate for anything this script would install or start. `curl | sh` # leaves stdin holding the script itself, so prompts must go through the # terminal; without one (CI, provisioning), --yes is the only way to consent. confirm() { [ "$ASSUME_YES" -eq 1 ] && { say "$1 (consented via --yes)"; return 0; } if [ -r /dev/tty ]; then printf '%s [Y/n] ' "$1" > /dev/tty answer="" read -r answer < /dev/tty || answer="" case "$answer" in n*|N*) return 1 ;; *) return 0 ;; esac fi die "$2. No terminal to ask on: re-run with --yes to consent, or fix it yourself and re-run." } # Every state-changing command funnels through here so --dry-run can print the # exact plan without touching the host. Read-only probes run directly. run() { if [ "$DRY_RUN" -eq 1 ]; then printf '\033[33mdry-run:\033[0m %s\n' "$1"; return 0; fi sh -c "$1" } # ── validate the request before touching anything ─────────────────────────── case "$HTTP_PORT" in ''|*[!0-9]*) die "--port needs a port number" ;; esac if [ -n "$LICENSE_KEY" ]; then case "$LICENSE_KEY" in lyk_*) ;; *) die "--license-key needs a lyk_... key (it is in your install pack)" ;; esac fi [ -n "$ORIGIN" ] || ORIGIN="http://localhost:$HTTP_PORT" case "$ORIGIN" in http://*|https://*) ;; *) die "--origin must start with http:// or https://" ;; esac case "$ORIGIN" in *[[:space:]]*) die "--origin must not contain whitespace" ;; esac # ── 1 · operating system ──────────────────────────────────────────────────── case "$(uname -s)" in Darwin) PLATFORM=macos ;; Linux) if grep -qi microsoft /proc/version 2>/dev/null; then case "$(uname -r)" in *microsoft-standard*|*WSL2*) PLATFORM=wsl2 ;; # WSL1 has no real Linux kernel, so no Docker daemon can ever run in # it; upgrading the distribution is the only fix, and it is one command. *) die "This is WSL1, which cannot run Docker. From PowerShell: wsl --set-version 2, then re-run" ;; esac else PLATFORM=linux fi ;; MINGW*|MSYS*|CYGWIN*) die "Use the Windows bootstrap from PowerShell instead: irm https://get.lyriks.io/ps | iex" ;; *) die "Unsupported OS: $(uname -s). Linux, macOS, or Windows via install.ps1" ;; esac # ── 2 · architecture ──────────────────────────────────────────────────────── ARCH="$(uname -m)" case "$ARCH" in x86_64|amd64) ;; arm64|aarch64) # Lyriks ships linux/amd64 images only. Docker Desktop on Apple Silicon # runs them through Rosetta, which works but is slower and not part of the # validated matrix; anywhere else there is no emulation story worth having. [ "$PLATFORM" = "macos" ] || die "Lyriks ships linux/amd64 images and this machine is $ARCH; arm64 is only supported on macOS (via Rosetta)" confirm "Apple Silicon runs Lyriks's x86-64 images through Rosetta emulation (slower, best effort). Continue?" \ "This is an Apple Silicon Mac and emulation was not consented to" ;; *) die "Unsupported architecture: $ARCH (Lyriks ships linux/amd64 images)" ;; esac ok "Machine: $ARCH on $PLATFORM" # ── 3 · Docker ────────────────────────────────────────────────────────────── # SKIP_PROBES: in --dry-run a proposed Docker install did not actually happen, # so later daemon probes would fail on a truth the plan already changed. The # plan keeps printing; the probes stand down. SKIP_PROBES=0 if ! command -v docker >/dev/null 2>&1; then case "$PLATFORM" in macos) # Homebrew is the one installer this script can drive; without it the # .dmg needs a human anyway, so a link is the honest answer. if command -v brew >/dev/null 2>&1; then confirm "Docker Desktop is missing. Install it via Homebrew (brew install --cask docker)?" "Docker is missing" run "brew install --cask docker" else die "Docker Desktop is not installed and Homebrew is absent. Install it from https://docs.docker.com/desktop/ then re-run this command" fi ;; wsl2) say "Docker is not visible inside WSL. Two ways to get it:" say " 1. Docker Desktop on Windows, then Settings > Resources > WSL integration for this distribution (recommended)" say " 2. Docker Engine directly inside this distribution (what this script can do now)" confirm "Install Docker Engine inside WSL via get.docker.com (uses sudo)?" "Docker is missing" run "curl -fsSL https://get.docker.com | sh" run "sudo usermod -aG docker \"${USER:-$(id -un)}\"" ;; linux) confirm "Docker is missing. Install Docker Engine now via get.docker.com (uses sudo)?" "Docker is missing" run "curl -fsSL https://get.docker.com | sh" run "sudo usermod -aG docker \"${USER:-$(id -un)}\"" ;; esac if ! command -v docker >/dev/null 2>&1; then [ "$DRY_RUN" -eq 1 ] || die "Docker is still not on PATH; open a new shell and re-run this command" SKIP_PROBES=1 fi fi # How to reach the daemon. A user freshly added to the docker group does not # hold it in this shell (membership is read at login), so `sg docker` bridges # exactly this run; from the next login plain docker works. Commands that need # the daemon therefore go through as_docker(). DOCKER_VIA="" as_docker() { if [ -n "$DOCKER_VIA" ]; then run "sg docker -c \"$1\""; else run "$1"; fi } start_daemon_hint() { if command -v systemctl >/dev/null 2>&1; then printf 'sudo systemctl enable --now docker' else printf 'sudo service docker start' fi } if [ "$SKIP_PROBES" -eq 0 ]; then if docker info >/dev/null 2>&1; then ok "Docker daemon reachable" elif [ "$DRY_RUN" -eq 1 ]; then # A plan preview must never die on fixable state: the real run diagnoses # (group membership, stopped daemon, Desktop integration) and proposes the # fix at this exact point. say "dry-run: the Docker daemon is not reachable right now; the real run diagnoses and proposes the fix here" SKIP_PROBES=1 elif [ "$PLATFORM" = "macos" ]; then # On macOS the daemon lives inside Docker Desktop: not running is the only # failure mode this probe can hit, and starting the app is the fix. confirm "Docker Desktop is not running. Start it now (open -a Docker)?" "Docker daemon is not reachable" run "open -a Docker" if [ "$DRY_RUN" -eq 0 ]; then say "Waiting for Docker Desktop (a first launch shows a dialog to accept) ..." waited=0 until docker info >/dev/null 2>&1; do [ "$waited" -ge 120 ] && die "Docker Desktop did not come up in 120s: finish its first-run dialog, wait for the whale icon to settle, then re-run this command" sleep 3; waited=$((waited + 3)) done ok "Docker daemon reachable" fi elif getent group docker 2>/dev/null | grep -qw "${USER:-$(id -un)}" && sg docker -c "docker info" >/dev/null 2>&1; then DOCKER_VIA=sg ok "Docker reachable through 'sg docker' (plain docker works from your next login)" elif [ "$PLATFORM" = "wsl2" ] && ! docker info >/dev/null 2>&1 && command -v docker.exe >/dev/null 2>&1; then die "Docker Desktop is on Windows but not wired into WSL: enable Settings > Resources > WSL integration for this distribution, Apply & restart, re-run" else confirm "The Docker daemon is not running. Start it now ($(start_daemon_hint))?" "Docker daemon is not reachable" run "$(start_daemon_hint)" if [ "$DRY_RUN" -eq 0 ] && ! docker info >/dev/null 2>&1 && ! { getent group docker 2>/dev/null | grep -qw "${USER:-$(id -un)}" && sg docker -c "docker info" >/dev/null 2>&1; }; then die "Docker daemon still not reachable. If you were just added to the docker group: log out and back in, then re-run" fi getent group docker 2>/dev/null | grep -qw "${USER:-$(id -un)}" && ! docker info >/dev/null 2>&1 && DOCKER_VIA=sg fi fi # ── 4 · Compose v2 and tar ────────────────────────────────────────────────── if [ "$SKIP_PROBES" -eq 0 ] && ! docker compose version >/dev/null 2>&1; then if [ "$PLATFORM" = "macos" ]; then die "Docker Desktop is present but 'docker compose' is not: update Docker Desktop (Compose v2 ships with it), then re-run" elif command -v apt-get >/dev/null 2>&1; then confirm "Compose v2 is missing. Install the plugin (sudo apt-get install docker-compose-plugin)?" "Compose v2 is missing" run "sudo apt-get update && sudo apt-get install -y docker-compose-plugin" elif command -v dnf >/dev/null 2>&1; then confirm "Compose v2 is missing. Install the plugin (sudo dnf install docker-compose-plugin)?" "Compose v2 is missing" run "sudo dnf install -y docker-compose-plugin" else die "Docker Compose v2 is missing and no known package manager was found: install the docker-compose-plugin for your distribution, then re-run" fi fi command -v tar >/dev/null 2>&1 || die "tar is required to unpack the kit: install it from your distribution, then re-run" # ── 5 · operator account and licence (interactive last chance) ────────────── # Every edition creates the operator account at install (the kit refuses an # install nobody can sign in to), so the email is required; a terminal gets # asked, automation must pass --admin-email, and a dry-run shows a placeholder. if [ -z "$ADMIN_EMAIL" ] && [ "$ASSUME_YES" -eq 0 ] && [ "$DRY_RUN" -eq 0 ] && [ -r /dev/tty ]; then printf 'Admin email (the operator account created at install): ' > /dev/tty read -r ADMIN_EMAIL < /dev/tty || ADMIN_EMAIL="" fi if [ -z "$ADMIN_EMAIL" ]; then if [ "$DRY_RUN" -eq 1 ]; then ADMIN_EMAIL="" else die "--admin-email is required: it creates the operator account this install signs in with" fi fi if [ -z "$LICENSE_KEY" ] && [ "$ASSUME_YES" -eq 0 ] && [ "$DRY_RUN" -eq 0 ] && [ -r /dev/tty ]; then printf 'Licence key (lyk_..., Enter to skip and activate later at /activate): ' > /dev/tty read -r LICENSE_KEY < /dev/tty || LICENSE_KEY="" if [ -n "$LICENSE_KEY" ]; then case "$LICENSE_KEY" in lyk_*) ;; *) die "That does not look like a licence key (lyk_...)" ;; esac fi fi # ── 6 · fetch the kit, install, smoke ─────────────────────────────────────── if [ "$EDITION" = "community" ]; then KIT_PROJECT=community; else KIT_PROJECT=enterprise; fi KIT_IMAGE="$REGISTRY_HOST/$KIT_PROJECT/appliance-kit:stable" # Outbound HTTPS to the registry is the one network dependency; probed here, a # blocked proxy or captive portal gets named instead of surfacing later as a # pull error that reads like a credentials problem. Any HTTP status proves # connectivity, so no -f: /v2/ answers 401 to anonymous callers by design. if [ "$DRY_RUN" -eq 0 ] && command -v curl >/dev/null 2>&1; then if curl -sSI --max-time 10 "https://$REGISTRY_HOST/v2/" >/dev/null 2>&1; then ok "Registry reachable ($REGISTRY_HOST)" else die "Cannot reach https://$REGISTRY_HOST over HTTPS: check your network or proxy, then re-run" fi fi # Re-running on an existing install is the supported repair path, and saying # so up front is what makes an anxious operator actually dare to do it. if [ -f "$LYRIKS_HOME/.env" ]; then say "Existing install detected in $LYRIKS_HOME: reconciling it (data lives in Docker volumes and is preserved)" fi say "Fetching the appliance kit into $LYRIKS_HOME ..." # Single quotes on purpose: these strings survive both execution paths (plain # `sh -c` and the `sg docker -c "..."` wrapper, where a double quote would # close the wrapper's own quoting). run "mkdir -p '$LYRIKS_HOME'" if ! as_docker "docker run --pull=always --rm $KIT_IMAGE | tar -x -C '$LYRIKS_HOME'"; then if [ "$EDITION" = "enterprise" ]; then die "Could not pull the kit. Enterprise images need the registry login from your pack (docker login $REGISTRY_HOST -u 'robot\$'), then re-run" fi die "Could not pull the kit from $KIT_IMAGE: check outbound HTTPS to $REGISTRY_HOST, then re-run" fi INSTALL_ARGS="--origin $ORIGIN" if [ "$HTTP_PORT" != "3000" ]; then INSTALL_ARGS="$INSTALL_ARGS --http-port $HTTP_PORT"; fi INSTALL_ARGS="$INSTALL_ARGS --admin-email $ADMIN_EMAIL" if [ -n "$WORKSPACE_NAME" ]; then INSTALL_ARGS="$INSTALL_ARGS --workspace-name '$WORKSPACE_NAME'"; fi if [ -n "$LICENSE_KEY" ]; then INSTALL_ARGS="$INSTALL_ARGS --license-key $LICENSE_KEY"; fi as_docker "cd '$LYRIKS_HOME' && ./lyriks doctor" as_docker "cd '$LYRIKS_HOME' && ./lyriks install $EDITION $INSTALL_ARGS" as_docker "cd '$LYRIKS_HOME' && ./lyriks smoke" # ── 7 · what now ──────────────────────────────────────────────────────────── # This block only runs when smoke passed: set -e aborts on any earlier step, # so success is never announced before the appliance proved it can be used. if [ "$DRY_RUN" -eq 1 ]; then say "dry-run complete: a real run executes the plan above" else printf '\n🚀 Lyriks is ready\n%s\n\n' "$ORIGIN" fi say "Sign in as $ADMIN_EMAIL with the temporary password printed above (shown once), then choose your own" if [ -z "$LICENSE_KEY" ]; then say "Activate: sign in, then paste your licence key at $ORIGIN/activate"; fi if [ "$FROM_WINDOWS" -eq 0 ]; then say "AI client (optional, needs Node 18+): claude mcp add --scope user --transport http lyriks $ORIGIN/mcp" fi say "Manage it: cd $LYRIKS_HOME && ./lyriks status | start | stop | update | backup create. Remove it: ./lyriks destroy --confirm"