mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
* fix(selfhost): read the published host port back from Compose (MUL-5505)
`make selfhost` polled the wrong port when the backend host port was passed
through the environment. Make and Compose disagree on variable precedence: an
assignment in an included makefile (the Makefile `include`s .env) overrides a
real environment variable, while for Compose the environment overrides .env.
Because .env.example ships BACKEND_PORT uncommented, .env always pinned the
recipe's view of the port, so `BACKEND_PORT=9000 make selfhost` published the
stack on 9000 while the health check hammered 8080 for 60s and then reported
"Services are still starting" on a perfectly healthy install. The success
message printed the same wrong backend URL. FRONTEND_PORT had the same
inversion, affecting the printed frontend URL.
Stop re-deriving the host port in the recipe and ask Compose, which is the only
authority on what it published. The wait-and-report block (three port
references per target, duplicated across selfhost and selfhost-build) moves into
scripts/selfhost-wait.sh, which resolves both host ports via `docker compose
port` and falls back to the compose default chain when the stack is not up.
Also fix the input contract: `PORT` was the only port variable documented in
SELF_HOSTING_ADVANCED.md, yet compose read only BACKEND_PORT, so setting the
documented variable was silently ignored. The backend host port mapping now
falls back to `${PORT:-8080}`, matching Makefile and scripts/local-env.sh, and
the docs describe both variables as host ports.
Container-internal ports (8080/3000), the global PORT derivation used by local
dev, and the 127.0.0.1 bindings are unchanged; the default self-host experience
is byte-identical.
Fixes #6145
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): make PORT the real backend port and test the whole chain
Addresses review feedback on #6168.
The first round's root-cause claim was wrong. It said `BACKEND_PORT=9100 make
selfhost` published on 9100 while the health check probed 8080. It does not:
when make drives Compose, Compose inherits make's exported values, so both
sides agree on 8080 and the override is silently dropped instead. Re-measured
through the real recipe, the genuine disagreements were:
- `.env` setting PORT with no BACKEND_PORT: probe followed PORT, Compose
published ${BACKEND_PORT:-8080} — 9100 vs 8080.
- `make selfhost PORT=8080` over a .env with BACKEND_PORT=9100: probe 8080,
Compose published 9100.
Both are the "wrong port variable" defect the GitHub issue names, and reading
the port back from Compose fixes both. The comments and PR text that blamed
make/Compose precedence are corrected.
The PORT fallback added to docker-compose.selfhost.yml was also unreachable:
.env.example shipped BACKEND_PORT=8080 uncommented, so the fallback never
engaged and `SELF_HOSTING_AI.md`'s "edit PORT and FRONTEND_PORT" instruction did
nothing. Pick one contract and apply it everywhere: PORT is the backend port to
edit, BACKEND_PORT/API_PORT/SERVER_PORT are optional aliases that override it.
That is already what Makefile, scripts/local-env.sh, scripts/install.sh and
install.ps1 do; compose and the web dev fallback were the outliers.
- .env.example ships BACKEND_PORT commented out, like its sibling aliases, so
editing PORT works out of the box.
- apps/web/config/runtime-urls.ts walks the same alias chain instead of
reading BACKEND_PORT alone, so `pnpm dev` follows an edited PORT.
- SELF_HOSTING_AI.md and SELF_HOSTING_ADVANCED.md state the contract.
Configuration that cannot take effect is now reported instead of ignored.
scripts/selfhost-preflight.sh warns when an alias in the env file overrides an
edited PORT, and when a port from the shell environment is overridden by the env
file. The Makefile captures the pristine origins before its include, because
afterwards there is no way to tell that `PORT=9000 make selfhost` was asked for.
Tests now cross environment -> make -> compose -> report for real. The docker
stub is no longer told what to publish: on `up` it asks the real
`docker compose config` to interpolate the compose file with the environment the
recipe actually handed it, records that, and answers `port` from the recording.
Verified failing on the old code — with the old recipe and compose file the
suite reports published 8080 against probe 9100, the exact defect.
Co-authored-by: multica-agent <github@multica.ai>
* ci: gate the self-host test on scripts/selfhost-preflight.sh too
The new preflight script was missing from the frontend path filter, so a
change to it alone would skip the test that covers it.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): honour the full backend port alias chain in Compose
Addresses the second review round on #6168.
The contract this PR documents is
BACKEND_PORT -> API_PORT -> SERVER_PORT -> PORT -> 8080
and Makefile, scripts/local-env.sh, scripts/install.sh, scripts/install.ps1 and
apps/web/config/runtime-urls.ts all implement it. docker-compose.selfhost.yml
implemented only BACKEND_PORT -> PORT -> 8080, so `API_PORT=9100` or
`SERVER_PORT=9200` in .env still published 8080.
That is not only a direct-Compose concern. scripts/install.sh:407 derives its
health-check port from the full chain and then runs this compose file, so an
alias it honours but Compose ignores puts the probe on 9100 while the stack
listens on 8080 — the original #6145 defect, reached through the installer.
- docker-compose.selfhost.yml publishes the full nested chain, verified to
keep the same precedence and the same 8080 default.
- scripts/selfhost-wait.sh's fallback matches, so the degraded path agrees
with what Compose would have published.
- The Makefile captures the pristine origins of API_PORT and SERVER_PORT too,
and the preflight reports them, so no alias can be silently overridden by
the env file while the others are reported.
Tests pin the chain on the boundaries a recipe-level test cannot see, because
Makefile normalises all four aliases into PORT before any recipe runs: a matrix
over the direct Compose path asserts each alias moves the published port with
the right precedence, and every case additionally asserts that
scripts/install.sh's resolver returns the same value Compose published. That
invariant is the one that actually protects the installer.
Verified failing without the fix: with the two-level chain restored the suite
reports `expected 9200 / compose published 8080 / installer resolved 9200`, and
with the alias warnings narrowed it reports the missing API_PORT notice.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): resolve one winner before reporting ignored port config
Addresses the third review round on #6168.
The preflight walked each alias independently, so it made a separate "wins"
claim per alias. With
PORT=9000
BACKEND_PORT=8000
API_PORT=7000
SERVER_PORT=6000
in .env it announced that BACKEND_PORT, API_PORT and SERVER_PORT each won, when
only BACKEND_PORT=8000 can. Two of the three notices were simply false.
It also compared only same-named shell and file variables, so shadowing across
aliases stayed silent: .env with PORT=8000 and BACKEND_PORT=8000 plus a shell
API_PORT=7000 produced no output at all, even though API_PORT was discarded.
Resolve the winner along the documented chain first — within a variable the env
file beats the environment, then BACKEND_PORT beats API_PORT beats SERVER_PORT
beats PORT — and report only the inputs that winner actually shadows. Output now
names one winning input and lists the rest, whichever variable or source they
came from. An input carrying the winning value is not reported: it is redundant
but the user still gets the port they asked for, so there is nothing to fix.
Tests cover both cases the old logic got wrong: every alias set at once must
yield exactly one winner claim and list the others as unused, and an env-file
alias beating a lower-priority shell alias must be reported. Also asserted that
a redundant same-value alias and a default configuration both stay silent.
Measured against the previous implementation, phrasing aside: with all four
values set it made 3 winner claims where there is now 1, and on the cross-alias
case it printed nothing where API_PORT is now reported.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): track env-file existence so empty port assignments resolve right
Addresses the fourth review round on #6168.
The Makefile passed only values to the preflight, so the script inferred "the
env file does not set this" from an empty string. An explicit empty assignment is
a different input from an absent one: make lets `BACKEND_PORT=` in the env file
override `BACKEND_PORT=9000` from the environment, and the variable then drops out
of the alias chain instead of setting a port.
With .env holding PORT=8080 and BACKEND_PORT=, invoked as
`BACKEND_PORT=9000 make selfhost`, the stack published 8080 and the health check
probed 8080 — correct — while the preflight announced
the backend host port resolves to 9000 from BACKEND_PORT (environment)
Set but unused: PORT=8080 (.env)
naming the ignored value as the winner and the winning value as unused. A report
that contradicts the startup is worse than no report. FRONTEND_PORT= had the
matching hole: it shadowed the environment, Compose fell back to 3000, and the
preflight said nothing.
The Makefile now captures ENV_FILE_<VAR>_IS_SET from $(origin) alongside the
value, for all five port variables via one $(foreach) instead of hand-written
lines. The preflight treats a variable the file defines as taking the file's
value even when empty, so it shadows the environment, and an empty value never
wins — resolution continues down the chain and falls back to 8080. Inputs the
winner shadows are still listed, including ones shadowed by an empty assignment.
The frontend check mirrors this and now names the port that actually resolved.
Recipe-level tests cover an empty alias over a shell value, every link of the
chain emptied at once, and the frontend equivalent — each asserting the reported
winner, the Compose published port and the probed port all agree. Against the
previous implementation the first case fails with `reported: 9000 / actual: 8080`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): installers read the published port; drop the second parser
Addresses all four blockers from the consolidated review on #6168.
1. Both installers probed and printed the wrong port. scripts/install.sh:402 and
scripts/install.ps1 start Compose inheriting the current environment, and
Compose lets that environment outrank .env — but the installers then derived
the probe port and the summary URL from .env alone. Measured on this branch
before the fix, every port variable diverged:
ambient PORT=9100 -> compose 9100, installer 8080
ambient BACKEND_PORT=9200 -> compose 9200, installer 8080
ambient API_PORT=9300 -> compose 9300, installer 8080
ambient SERVER_PORT=9400 -> compose 9400, installer 8080
ambient FRONTEND_PORT=3100 -> compose 3100, installer 3000
Both now read the ports back once with `docker compose port` after `up -d`
and reuse that single result for the health check and the summary. If the
query fails they fail loudly instead of claiming success. The .env-derived
helpers are deleted rather than left beside the new path.
2. The Make preflight is removed entirely, as the review recommended. It
modelled `origin=environment` and `origin=file` but not `command line`, so
`make selfhost BACKEND_PORT=9000` over a .env with API_PORT=7000 announced
7000 while Compose published 9000. That was its fourth wrong report in four
rounds, because it was a second parser of precedence rules — the very thing
this PR removes elsewhere. `docker compose port` is the runtime truth, so the
script, the Makefile captures and the macro are gone.
3. Tests now cover the installer paths that were unguarded. scripts/install.test.sh
gains a `--with-server` matrix (defaults, .env PORT, all four backend aliases,
FRONTEND_PORT, ambient overriding .env for all five, and explicit-empty
fallback) plus a case proving an unresolvable port fails loudly. A new
scripts/install.ps1.test.ps1 drives the same matrix through Start-LocalInstall.
Every case asserts Compose's published port == the probed URL == the printed
URL. Ambient variables are explicitly cleared per case so a runner's own PORT
cannot leak. The Makefile matrix gains the command-line origin cases. CI runs
the PowerShell suite on windows-latest in the always-on installer job, and the
frontend filter now includes both installers.
4. Docs state the real contract: the alias order is shared, but which *source*
wins is per entry point — Compose prefers the environment, make prefers the
included env file, and a make command-line assignment outranks both. The
removed preflight's promise is gone from SELF_HOSTING_AI.md, and the compose
header no longer claims the installer derives its own health-check port.
Verified failing without the fixes: the Bash matrix reports
`[ambient PORT beats .env] compose published 9500 / probed 9100 / printed 9100`
against the old installer, and the PowerShell matrix reports
`printed backend=8080` against an injected summary divergence.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): keep web dev proxy off frontend port
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
554 lines
19 KiB
Bash
Executable File
554 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Multica installer — installs the CLI and optionally provisions a self-host server.
|
|
#
|
|
# Install / upgrade CLI only:
|
|
# curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
|
#
|
|
# Install CLI + provision self-host server:
|
|
# curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
|
|
#
|
|
# After installation, run `multica setup` to configure your environment.
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
REPO_URL="https://github.com/multica-ai/multica.git"
|
|
REPO_WEB_URL="https://github.com/multica-ai/multica" # without .git, for GitHub web APIs
|
|
INSTALL_DIR="${MULTICA_INSTALL_DIR:-$HOME/.multica/server}"
|
|
BREW_PACKAGE="multica-ai/tap/multica"
|
|
|
|
# Host ports Compose reported after `up -d`; set by setup_server and reused by
|
|
# the summary so the health check and the printed URLs cannot diverge.
|
|
SELFHOST_BACKEND_PORT=""
|
|
SELFHOST_FRONTEND_PORT=""
|
|
|
|
# Colors (disabled when not a terminal)
|
|
if [ -t 1 ] || [ -t 2 ]; then
|
|
BOLD='\033[1m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
RED='\033[0;31m'
|
|
CYAN='\033[0;36m'
|
|
RESET='\033[0m'
|
|
else
|
|
BOLD='' GREEN='' YELLOW='' RED='' CYAN='' RESET=''
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
info() { printf "${BOLD}${CYAN}==> %s${RESET}\n" "$*"; }
|
|
ok() { printf "${BOLD}${GREEN}✓ %s${RESET}\n" "$*"; }
|
|
warn() { printf "${BOLD}${YELLOW}⚠ %s${RESET}\n" "$*" >&2; }
|
|
fail() { printf "${BOLD}${RED}✗ %s${RESET}\n" "$*" >&2; exit 1; }
|
|
|
|
command_exists() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
running_in_ssh_session() {
|
|
[ -n "${SSH_CONNECTION:-}" ] || [ -n "${SSH_CLIENT:-}" ] || [ -n "${SSH_TTY:-}" ]
|
|
}
|
|
|
|
print_remote_server_token_hint() {
|
|
if ! running_in_ssh_session; then
|
|
return
|
|
fi
|
|
|
|
printf " ${BOLD}Looks like a remote/SSH session.${RESET} Browser login may not be able to call back to this machine's localhost.\n"
|
|
printf " Token login is usually simpler here:\n"
|
|
printf " 1. On your local computer, open ${CYAN}https://multica.ai/settings?tab=tokens${RESET}\n"
|
|
printf " and create a token under ${BOLD}Settings > API Tokens${RESET}.\n"
|
|
printf " 2. On this server, run:\n"
|
|
printf " ${CYAN}multica login --token <YOUR_TOKEN>${RESET}\n"
|
|
printf " ${CYAN}multica daemon start${RESET}\n"
|
|
printf "\n"
|
|
}
|
|
|
|
# Host port Docker Compose actually published for a service.
|
|
#
|
|
# This is the only authority. Compose's interpolation gives the calling process
|
|
# environment precedence over .env, so an ambient PORT / BACKEND_PORT / API_PORT
|
|
# / SERVER_PORT / FRONTEND_PORT moves the published port without touching the
|
|
# file. Re-deriving the port from .env alone made the installer probe and print
|
|
# a port the stack was never published on (#6145). Must be called from the
|
|
# installation directory, after `up -d`.
|
|
compose_published_port() {
|
|
local service=$1 container_port=$2 published
|
|
|
|
published="$(docker compose -f docker-compose.selfhost.yml port "$service" "$container_port" 2>/dev/null | tail -n 1)"
|
|
published="${published##*:}"
|
|
published="${published%$'\r'}"
|
|
|
|
case "$published" in
|
|
"" | *[!0-9]*) return 1 ;;
|
|
esac
|
|
|
|
printf "%s" "$published"
|
|
}
|
|
|
|
detect_os() {
|
|
case "$(uname -s)" in
|
|
Darwin) OS="darwin" ;;
|
|
Linux) OS="linux" ;;
|
|
MINGW*|MSYS*|CYGWIN*)
|
|
fail "This script does not support Windows. Use the PowerShell installer instead:
|
|
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex" ;;
|
|
*) fail "Unsupported operating system: $(uname -s). Multica supports macOS, Linux, and Windows." ;;
|
|
esac
|
|
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64) ARCH="amd64" ;;
|
|
aarch64) ARCH="arm64" ;;
|
|
arm64) ARCH="arm64" ;;
|
|
*) fail "Unsupported architecture: $ARCH" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI Installation
|
|
# ---------------------------------------------------------------------------
|
|
_dump_brew_log() {
|
|
local log="$1"
|
|
if [ -s "$log" ]; then
|
|
warn "Homebrew output (last 80 lines):"
|
|
tail -n 80 "$log" | sed 's/^/ /' >&2
|
|
fi
|
|
}
|
|
|
|
install_cli_brew() {
|
|
info "Installing Multica CLI via Homebrew..."
|
|
local brew_log
|
|
brew_log=$(mktemp)
|
|
if ! brew tap multica-ai/tap >"$brew_log" 2>&1; then
|
|
warn "Failed to add Homebrew tap. Falling back to GitHub Releases binary install."
|
|
_dump_brew_log "$brew_log"
|
|
rm -f "$brew_log"
|
|
return 1
|
|
fi
|
|
# brew install exits non-zero if already installed on older Homebrew versions
|
|
if ! brew install "$BREW_PACKAGE" >"$brew_log" 2>&1; then
|
|
if brew list "$BREW_PACKAGE" >/dev/null 2>&1; then
|
|
rm -f "$brew_log"
|
|
ok "Multica CLI already installed via Homebrew"
|
|
else
|
|
warn "Failed to install multica via Homebrew. Falling back to GitHub Releases binary install."
|
|
_dump_brew_log "$brew_log"
|
|
rm -f "$brew_log"
|
|
return 1
|
|
fi
|
|
else
|
|
rm -f "$brew_log"
|
|
ok "Multica CLI installed via Homebrew"
|
|
fi
|
|
}
|
|
|
|
install_cli_binary() {
|
|
info "Installing Multica CLI from GitHub Releases..."
|
|
|
|
# Get latest release tag
|
|
local latest
|
|
latest=$(curl -sI "$REPO_WEB_URL/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n' || true)
|
|
if [ -z "$latest" ]; then
|
|
fail "Could not determine latest release. Check your network connection."
|
|
fi
|
|
|
|
local version="${latest#v}"
|
|
local url="https://github.com/multica-ai/multica/releases/download/${latest}/multica-cli-${version}-${OS}-${ARCH}.tar.gz"
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
|
|
info "Downloading $url ..."
|
|
if ! curl -fsSL "$url" -o "$tmp_dir/multica.tar.gz"; then
|
|
rm -rf "$tmp_dir"
|
|
fail "Failed to download CLI binary."
|
|
fi
|
|
|
|
tar -xzf "$tmp_dir/multica.tar.gz" -C "$tmp_dir" multica
|
|
|
|
# Try /usr/local/bin first, fall back to ~/.local/bin. Tests and scripted
|
|
# installs can override the first choice with MULTICA_BIN_DIR.
|
|
local bin_dir="${MULTICA_BIN_DIR:-/usr/local/bin}"
|
|
if [ -w "$bin_dir" ]; then
|
|
mv "$tmp_dir/multica" "$bin_dir/multica"
|
|
elif command_exists sudo; then
|
|
sudo mv "$tmp_dir/multica" "$bin_dir/multica"
|
|
else
|
|
bin_dir="$HOME/.local/bin"
|
|
mkdir -p "$bin_dir"
|
|
mv "$tmp_dir/multica" "$bin_dir/multica"
|
|
chmod +x "$bin_dir/multica"
|
|
# Add to PATH if not already there
|
|
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$bin_dir$"; then
|
|
export PATH="$bin_dir:$PATH"
|
|
add_to_path "$bin_dir"
|
|
fi
|
|
fi
|
|
|
|
rm -rf "$tmp_dir"
|
|
ok "Multica CLI installed to $bin_dir/multica"
|
|
}
|
|
|
|
add_to_path() {
|
|
local dir="$1"
|
|
local line="export PATH=\"$dir:\$PATH\""
|
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
if [ -f "$rc" ] && ! grep -qF "$dir" "$rc"; then
|
|
printf '\n# Added by Multica installer\n%s\n' "$line" >> "$rc"
|
|
fi
|
|
done
|
|
}
|
|
|
|
get_latest_version() {
|
|
# grep exits 1 when no match; use `|| true` to avoid triggering pipefail
|
|
curl -sI "$REPO_WEB_URL/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n' || true
|
|
}
|
|
|
|
get_selfhost_ref() {
|
|
if [ -n "${MULTICA_SELFHOST_REF:-}" ]; then
|
|
printf '%s' "$MULTICA_SELFHOST_REF"
|
|
return
|
|
fi
|
|
|
|
local latest
|
|
latest=$(get_latest_version)
|
|
if [ -n "$latest" ]; then
|
|
printf '%s' "$latest"
|
|
return
|
|
fi
|
|
|
|
printf '%s' "main"
|
|
}
|
|
|
|
checkout_server_ref() {
|
|
local ref="$1"
|
|
|
|
if [ "$ref" = "main" ]; then
|
|
git fetch origin main --depth 1 2>/dev/null || true
|
|
git checkout --force main 2>/dev/null || true
|
|
git reset --hard origin/main 2>/dev/null || true
|
|
return
|
|
fi
|
|
|
|
git fetch origin --tags --force 2>/dev/null || true
|
|
if git rev-parse --verify --quiet "refs/tags/$ref" >/dev/null; then
|
|
git checkout --force "$ref" 2>/dev/null || git checkout --force "tags/$ref" 2>/dev/null || true
|
|
return
|
|
fi
|
|
|
|
git fetch origin "$ref" --depth 1 2>/dev/null || true
|
|
git checkout --force "$ref" 2>/dev/null || true
|
|
}
|
|
|
|
pull_official_selfhost_images() {
|
|
if docker compose -f docker-compose.selfhost.yml pull; then
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
warn "Official images for the selected self-host channel are not published yet."
|
|
echo "This can happen before the first GHCR release is available."
|
|
echo "From $INSTALL_DIR, build from source instead:"
|
|
echo " docker compose -f docker-compose.selfhost.yml -f docker-compose.selfhost.build.yml up -d --build"
|
|
exit 1
|
|
}
|
|
|
|
upgrade_cli_brew() {
|
|
info "Upgrading Multica CLI via Homebrew..."
|
|
brew update 2>/dev/null || true
|
|
if brew upgrade "$BREW_PACKAGE" 2>/dev/null; then
|
|
ok "Multica CLI upgraded via Homebrew"
|
|
else
|
|
# brew upgrade exits non-zero if already up to date
|
|
ok "Multica CLI is already the latest version"
|
|
fi
|
|
}
|
|
|
|
install_cli() {
|
|
if command_exists multica; then
|
|
local current_ver
|
|
# `multica version` outputs "multica 0.3.23 (commit: f46b929eb, built: 2026-06-16T10:11:56Z)" — extract just the version
|
|
current_ver=$(multica version 2>/dev/null | awk 'NR==1{print $2}' || echo "unknown")
|
|
|
|
local latest_ver
|
|
latest_ver=$(get_latest_version)
|
|
|
|
# Normalize: strip leading 'v' for comparison
|
|
local current_cmp="${current_ver#v}"
|
|
local latest_cmp="${latest_ver#v}"
|
|
|
|
if [ -z "$latest_ver" ] || [ "$current_cmp" = "$latest_cmp" ]; then
|
|
ok "Multica CLI is up to date ($current_ver)"
|
|
return 0
|
|
fi
|
|
|
|
info "Multica CLI $current_ver installed, latest is $latest_ver — upgrading..."
|
|
if command_exists brew && brew list "$BREW_PACKAGE" >/dev/null 2>&1; then
|
|
upgrade_cli_brew
|
|
else
|
|
install_cli_binary
|
|
fi
|
|
|
|
local new_ver
|
|
new_ver=$(multica version 2>/dev/null | awk 'NR==1{print $2}' || echo "unknown")
|
|
ok "Multica CLI upgraded ($current_ver → $new_ver)"
|
|
return 0
|
|
fi
|
|
|
|
if command_exists brew; then
|
|
install_cli_brew || install_cli_binary
|
|
else
|
|
install_cli_binary
|
|
fi
|
|
|
|
# Verify
|
|
if ! command_exists multica; then
|
|
fail "CLI installed but 'multica' not found on PATH. You may need to restart your shell."
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Docker check
|
|
# ---------------------------------------------------------------------------
|
|
check_docker() {
|
|
if ! command_exists docker; then
|
|
printf "\n"
|
|
fail "Docker is not installed. Multica self-hosting requires Docker and Docker Compose.
|
|
|
|
Install Docker:
|
|
macOS: https://docs.docker.com/desktop/install/mac-install/
|
|
Linux: https://docs.docker.com/engine/install/
|
|
|
|
After installing Docker, re-run this script with --with-server."
|
|
fi
|
|
|
|
if ! docker info >/dev/null 2>&1; then
|
|
fail "Docker is installed but not running. Please start Docker and re-run this script."
|
|
fi
|
|
|
|
ok "Docker is available"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server setup (self-host / --with-server)
|
|
# ---------------------------------------------------------------------------
|
|
setup_server() {
|
|
info "Setting up Multica server..."
|
|
local server_ref
|
|
server_ref=$(get_selfhost_ref)
|
|
info "Using self-host assets from ${server_ref}..."
|
|
|
|
if [ -d "$INSTALL_DIR/.git" ]; then
|
|
info "Updating existing installation at $INSTALL_DIR..."
|
|
cd "$INSTALL_DIR"
|
|
else
|
|
info "Cloning Multica repository..."
|
|
if ! command_exists git; then
|
|
fail "Git is not installed. Please install git and re-run."
|
|
fi
|
|
# Remove leftover directory from a previously interrupted clone
|
|
if [ -d "$INSTALL_DIR" ]; then
|
|
warn "Removing incomplete installation at $INSTALL_DIR..."
|
|
rm -rf "$INSTALL_DIR"
|
|
fi
|
|
mkdir -p "$(dirname "$INSTALL_DIR")"
|
|
git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"
|
|
cd "$INSTALL_DIR"
|
|
fi
|
|
|
|
checkout_server_ref "$server_ref"
|
|
|
|
ok "Repository ready at $INSTALL_DIR ($server_ref)"
|
|
|
|
# Generate .env if needed
|
|
if [ ! -f .env ]; then
|
|
info "Creating .env with random secrets..."
|
|
cp .env.example .env
|
|
local jwt pgpass
|
|
jwt=$(openssl rand -hex 32)
|
|
pgpass=$(openssl rand -hex 24)
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=$jwt/" .env
|
|
sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$pgpass/" .env
|
|
sed -i '' -E "s#^(DATABASE_URL=postgres://[^:]+:)[^@]*(@.*)#\1$pgpass\2#" .env
|
|
else
|
|
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$jwt/" .env
|
|
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$pgpass/" .env
|
|
sed -i -E "s#^(DATABASE_URL=postgres://[^:]+:)[^@]*(@.*)#\1$pgpass\2#" .env
|
|
fi
|
|
ok "Generated .env with random JWT_SECRET and POSTGRES_PASSWORD"
|
|
else
|
|
ok "Using existing .env"
|
|
fi
|
|
|
|
# Start Docker Compose
|
|
info "Pulling official Multica images..."
|
|
pull_official_selfhost_images
|
|
info "Starting Multica services (this may take a few minutes on first run)..."
|
|
docker compose -f docker-compose.selfhost.yml up -d
|
|
|
|
# Read the ports Compose actually published, once, and reuse them for both the
|
|
# health check and the summary so the two can never disagree.
|
|
if ! SELFHOST_BACKEND_PORT="$(compose_published_port backend 8080)"; then
|
|
fail "Started the stack but could not read the backend host port from Docker Compose.
|
|
Check it with: cd $INSTALL_DIR && docker compose -f docker-compose.selfhost.yml ps"
|
|
fi
|
|
if ! SELFHOST_FRONTEND_PORT="$(compose_published_port frontend 3000)"; then
|
|
fail "Started the stack but could not read the frontend host port from Docker Compose.
|
|
Check it with: cd $INSTALL_DIR && docker compose -f docker-compose.selfhost.yml ps"
|
|
fi
|
|
|
|
# Wait for health check
|
|
info "Waiting for backend to be ready..."
|
|
local ready=false
|
|
for i in $(seq 1 45); do
|
|
if curl -sf "http://localhost:${SELFHOST_BACKEND_PORT}/health" >/dev/null 2>&1; then
|
|
ready=true
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [ "$ready" = true ]; then
|
|
ok "Multica server is running"
|
|
else
|
|
warn "Server is still starting. You can check logs with:"
|
|
echo " cd $INSTALL_DIR && docker compose -f docker-compose.selfhost.yml logs"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main: Default mode (install / upgrade CLI only)
|
|
# ---------------------------------------------------------------------------
|
|
run_default() {
|
|
printf "\n"
|
|
printf "${BOLD} Multica — Installer${RESET}\n"
|
|
printf "\n"
|
|
|
|
detect_os
|
|
install_cli
|
|
|
|
printf "\n"
|
|
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
printf "${BOLD}${GREEN} ✓ Multica CLI is ready!${RESET}\n"
|
|
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
printf "\n"
|
|
printf " ${BOLD}Next: configure your environment${RESET}\n"
|
|
printf "\n"
|
|
printf " ${CYAN}multica setup${RESET} # Connect to Multica Cloud (multica.ai)\n"
|
|
printf " ${CYAN}multica setup self-host${RESET} # Connect to a self-hosted server\n"
|
|
printf "\n"
|
|
print_remote_server_token_hint
|
|
printf " ${BOLD}Self-hosting?${RESET} Install the server first:\n"
|
|
printf " curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server\n"
|
|
printf "\n"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main: With-server mode (provision self-host infrastructure + install CLI)
|
|
# ---------------------------------------------------------------------------
|
|
run_with_server() {
|
|
printf "\n"
|
|
printf "${BOLD} Multica — Self-Host Installer${RESET}\n"
|
|
printf " Provisioning server infrastructure + installing CLI\n"
|
|
printf "\n"
|
|
|
|
detect_os
|
|
check_docker
|
|
setup_server
|
|
install_cli
|
|
|
|
printf "\n"
|
|
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
printf "${BOLD}${GREEN} ✓ Multica server is running and CLI is ready!${RESET}\n"
|
|
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
printf "\n"
|
|
printf " ${BOLD}Frontend:${RESET} http://localhost:%s\n" "$SELFHOST_FRONTEND_PORT"
|
|
printf " ${BOLD}Backend:${RESET} http://localhost:%s\n" "$SELFHOST_BACKEND_PORT"
|
|
printf " ${BOLD}Server at:${RESET} %s\n" "$INSTALL_DIR"
|
|
printf "\n"
|
|
printf " ${BOLD}Next: configure your CLI to connect${RESET}\n"
|
|
printf "\n"
|
|
printf " ${CYAN}multica setup self-host${RESET} # Configure + authenticate + start daemon\n"
|
|
printf "\n"
|
|
printf " ${BOLD}Login:${RESET} configure ${CYAN}RESEND_API_KEY${RESET} in .env for email codes,\n"
|
|
printf " or read the generated code from backend logs when Resend is unset.\n"
|
|
printf "\n"
|
|
printf " ${BOLD}To stop all services:${RESET}\n"
|
|
printf " curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --stop\n"
|
|
printf "\n"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stop: shut down a self-hosted installation
|
|
# ---------------------------------------------------------------------------
|
|
run_stop() {
|
|
printf "\n"
|
|
info "Stopping Multica services..."
|
|
|
|
if [ -d "$INSTALL_DIR" ]; then
|
|
cd "$INSTALL_DIR"
|
|
if [ -f docker-compose.selfhost.yml ]; then
|
|
docker compose -f docker-compose.selfhost.yml down
|
|
ok "Docker services stopped"
|
|
else
|
|
warn "No docker-compose.selfhost.yml found at $INSTALL_DIR"
|
|
fi
|
|
else
|
|
warn "No Multica installation found at $INSTALL_DIR"
|
|
fi
|
|
|
|
if command_exists multica; then
|
|
multica daemon stop 2>/dev/null && ok "Daemon stopped" || true
|
|
fi
|
|
|
|
printf "\n"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
main() {
|
|
local mode="default"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--with-server) mode="with-server" ;;
|
|
--local) mode="with-server" ;; # backwards compat alias
|
|
--stop) mode="stop" ;;
|
|
--help|-h)
|
|
echo "Usage: install.sh [--with-server | --stop]"
|
|
echo ""
|
|
echo " (default) Install / upgrade the Multica CLI"
|
|
echo " --with-server Install CLI + provision a self-host server (Docker)"
|
|
echo " --stop Stop a self-hosted installation"
|
|
echo ""
|
|
echo "Environment variables:"
|
|
echo " MULTICA_INSTALL_DIR Self-host server install directory"
|
|
echo " (default: \$HOME/.multica/server)"
|
|
echo " MULTICA_BIN_DIR Target directory for the CLI binary when"
|
|
echo " installing from GitHub Releases"
|
|
echo " (default: /usr/local/bin, then \$HOME/.local/bin)"
|
|
echo " MULTICA_SELFHOST_REF Git ref to check out for self-host assets"
|
|
echo " (default: latest release tag, falling back to main)"
|
|
echo ""
|
|
echo "After installation, run 'multica setup' to configure your environment."
|
|
exit 0
|
|
;;
|
|
*) warn "Unknown option: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
case "$mode" in
|
|
default) run_default ;;
|
|
with-server) run_with_server ;;
|
|
stop) run_stop ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|