mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 09:30:05 +02:00
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>
581 lines
21 KiB
PowerShell
581 lines
21 KiB
PowerShell
# Multica installer for Windows — one command to get started.
|
|
#
|
|
# Install CLI (default): connects to multica.ai
|
|
# irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
|
|
#
|
|
# Self-host: starts a local Multica server + installs CLI + configures
|
|
# $env:MULTICA_MODE="local"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
|
|
#
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
$RepoUrl = "https://github.com/multica-ai/multica.git"
|
|
$RepoWebUrl = "https://github.com/multica-ai/multica"
|
|
$DefaultInstallDir = Join-Path $env:USERPROFILE ".multica\server"
|
|
$InstallDir = if ($env:MULTICA_INSTALL_DIR) { $env:MULTICA_INSTALL_DIR } else { $DefaultInstallDir }
|
|
|
|
# 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.
|
|
$script:SelfHostBackendPort = $null
|
|
$script:SelfHostFrontendPort = $null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
function Write-Info { param([string]$Msg) Write-Host "==> $Msg" -ForegroundColor Cyan }
|
|
function Write-Ok { param([string]$Msg) Write-Host "[OK] $Msg" -ForegroundColor Green }
|
|
function Write-Warn { param([string]$Msg) Write-Warning $Msg }
|
|
function Write-Fail { param([string]$Msg) Write-Host "[ERROR] $Msg" -ForegroundColor Red; exit 1 }
|
|
|
|
function Test-CommandExists {
|
|
param([string]$Name)
|
|
$null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function New-RandomHex {
|
|
param([int]$ByteCount)
|
|
|
|
$bytes = New-Object byte[] $ByteCount
|
|
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
|
try {
|
|
$rng.GetBytes($bytes)
|
|
} finally {
|
|
$rng.Dispose()
|
|
}
|
|
return -join ($bytes | ForEach-Object { "{0:x2}" -f $_ })
|
|
}
|
|
|
|
# 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`.
|
|
function Get-ComposePublishedPort {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Service,
|
|
[Parameter(Mandatory = $true)][int]$ContainerPort
|
|
)
|
|
|
|
$output = $null
|
|
try {
|
|
$output = docker compose -f docker-compose.selfhost.yml port $Service $ContainerPort 2>$null
|
|
} catch {
|
|
return $null
|
|
}
|
|
if ($LASTEXITCODE -ne 0) {
|
|
return $null
|
|
}
|
|
|
|
$line = @($output | Where-Object { $_ }) | Select-Object -Last 1
|
|
if (-not $line) {
|
|
return $null
|
|
}
|
|
|
|
$published = ($line -split ":")[-1].Trim()
|
|
if ($published -notmatch '^[0-9]+$') {
|
|
return $null
|
|
}
|
|
return $published
|
|
}
|
|
|
|
function Get-LatestVersion {
|
|
try {
|
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/multica-ai/multica/releases/latest" -ErrorAction Stop
|
|
return $release.tag_name
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Get-SelfHostRef {
|
|
if ($env:MULTICA_SELFHOST_REF) {
|
|
return $env:MULTICA_SELFHOST_REF
|
|
}
|
|
|
|
$latest = Get-LatestVersion
|
|
if ($latest) {
|
|
return $latest
|
|
}
|
|
|
|
return "main"
|
|
}
|
|
|
|
function Checkout-ServerRef {
|
|
param([string]$Ref)
|
|
|
|
if ($Ref -eq "main") {
|
|
git fetch origin main --depth 1 2>$null
|
|
git checkout --force main 2>$null
|
|
git reset --hard origin/main 2>$null
|
|
return
|
|
}
|
|
|
|
git fetch origin --tags --force 2>$null
|
|
$tagRef = "refs/tags/$Ref"
|
|
git show-ref --verify --quiet $tagRef 2>$null
|
|
if ($LASTEXITCODE -eq 0) {
|
|
git checkout --force $Ref 2>$null
|
|
return
|
|
}
|
|
|
|
git fetch origin $Ref --depth 1 2>$null
|
|
git checkout --force $Ref 2>$null
|
|
}
|
|
|
|
function Pull-OfficialSelfHostImages {
|
|
docker compose -f docker-compose.selfhost.yml pull
|
|
if ($LASTEXITCODE -eq 0) {
|
|
return
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Warn "Official images for the selected self-host channel are not published yet."
|
|
Write-Host "This can happen before the first GHCR release is available."
|
|
Write-Host "From $InstallDir, build from source instead:"
|
|
Write-Host " docker compose -f docker-compose.selfhost.yml -f docker-compose.selfhost.build.yml up -d --build"
|
|
exit 1
|
|
}
|
|
|
|
function Convert-ToCliArch {
|
|
param([object]$Value)
|
|
|
|
if ($null -eq $Value) {
|
|
return $null
|
|
}
|
|
|
|
$normalized = "$Value".Trim().ToUpperInvariant()
|
|
switch ($normalized) {
|
|
"9" { return "amd64" }
|
|
"AMD64" { return "amd64" }
|
|
"X64" { return "amd64" }
|
|
"X86_64" { return "amd64" }
|
|
"12" { return "arm64" }
|
|
"ARM64" { return "arm64" }
|
|
"AARCH64" { return "arm64" }
|
|
default { return $null }
|
|
}
|
|
}
|
|
|
|
function Get-WindowsCliArch {
|
|
$signals = @()
|
|
$nativeArchSignalFound = $false
|
|
|
|
# Prefer the native processor architecture over the current PowerShell
|
|
# process architecture. This keeps Windows on ARM from being misdetected
|
|
# when PowerShell is running through x64/x86 emulation.
|
|
try {
|
|
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
|
$processorArch = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop |
|
|
Select-Object -First 1 -ExpandProperty Architecture
|
|
$signals += [pscustomobject]@{ Source = "Win32_Processor.Architecture"; Value = $processorArch }
|
|
$nativeArchSignalFound = $true
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
if (-not $nativeArchSignalFound -and (Get-Command Get-WmiObject -ErrorAction SilentlyContinue)) {
|
|
$processorArch = Get-WmiObject -Class Win32_Processor -ErrorAction Stop |
|
|
Select-Object -First 1 -ExpandProperty Architecture
|
|
$signals += [pscustomobject]@{ Source = "Win32_Processor.Architecture"; Value = $processorArch }
|
|
$nativeArchSignalFound = $true
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
$signals += [pscustomobject]@{
|
|
Source = "RuntimeInformation.OSArchitecture"
|
|
Value = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
|
}
|
|
} catch {}
|
|
|
|
$signals += [pscustomobject]@{ Source = "PROCESSOR_ARCHITEW6432"; Value = $env:PROCESSOR_ARCHITEW6432 }
|
|
$signals += [pscustomobject]@{ Source = "PROCESSOR_ARCHITECTURE"; Value = $env:PROCESSOR_ARCHITECTURE }
|
|
|
|
foreach ($signal in $signals) {
|
|
$arch = Convert-ToCliArch $signal.Value
|
|
if ($arch) {
|
|
return $arch
|
|
}
|
|
}
|
|
|
|
$details = ($signals |
|
|
Where-Object { $null -ne $_.Value -and "$($_.Value)".Trim() -ne "" } |
|
|
ForEach-Object { "$($_.Source)=$($_.Value)" }) -join ", "
|
|
if (-not $details) {
|
|
$details = "no architecture signals available"
|
|
}
|
|
|
|
Write-Fail "Unsupported Windows architecture ($details). Only x64 and ARM64 are supported."
|
|
}
|
|
|
|
function Get-InstalledCliVersion {
|
|
try {
|
|
$firstLine = multica version 2>$null | Select-Object -First 1
|
|
if ("$firstLine" -match '\b(v?\d+(?:\.\d+)+)\b') {
|
|
$version = $Matches[1]
|
|
if ($version -notlike 'v*') {
|
|
$version = "v$version"
|
|
}
|
|
return $version
|
|
}
|
|
} catch {}
|
|
|
|
return $null
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI Installation
|
|
# ---------------------------------------------------------------------------
|
|
function Install-CliBinary {
|
|
Write-Info "Installing Multica CLI from GitHub Releases..."
|
|
|
|
if (-not [Environment]::Is64BitOperatingSystem) {
|
|
Write-Fail "Multica requires a 64-bit Windows installation."
|
|
}
|
|
|
|
$arch = Get-WindowsCliArch
|
|
|
|
$latest = Get-LatestVersion
|
|
if (-not $latest) {
|
|
Write-Fail "Could not determine latest release. Check your network connection."
|
|
}
|
|
|
|
$version = $latest.TrimStart('v')
|
|
$url = "https://github.com/multica-ai/multica/releases/download/$latest/multica-cli-$version-windows-$arch.zip"
|
|
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "multica-install"
|
|
|
|
if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force }
|
|
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
|
|
|
Write-Info "Downloading $url ..."
|
|
try {
|
|
Invoke-WebRequest -Uri $url -OutFile (Join-Path $tmpDir "multica.zip") -UseBasicParsing
|
|
} catch {
|
|
Remove-Item $tmpDir -Recurse -Force
|
|
Write-Fail "Failed to download CLI binary: $_"
|
|
}
|
|
|
|
# Verify SHA256 checksum
|
|
$checksumUrl = "https://github.com/multica-ai/multica/releases/download/$latest/checksums.txt"
|
|
try {
|
|
$checksums = Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing -ErrorAction Stop
|
|
$checksumContent = if ($checksums.Content -is [byte[]]) {
|
|
[System.Text.Encoding]::UTF8.GetString($checksums.Content)
|
|
} else {
|
|
[string]$checksums.Content
|
|
}
|
|
$zipFile = Join-Path $tmpDir "multica.zip"
|
|
$actualHash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash.ToLower()
|
|
$releaseAsset = "multica-cli-$version-windows-$arch.zip"
|
|
$legacyAsset = "multica_windows_$arch.zip"
|
|
$expectedLine = ($checksumContent -split "`r?`n") |
|
|
Where-Object {
|
|
$_ -match [regex]::Escape($releaseAsset) -or
|
|
$_ -match [regex]::Escape($legacyAsset)
|
|
} |
|
|
Select-Object -First 1
|
|
if ($expectedLine) {
|
|
$expectedHash = ($expectedLine -split "\s+")[0].ToLower()
|
|
if ($actualHash -ne $expectedHash) {
|
|
Remove-Item $tmpDir -Recurse -Force
|
|
Write-Fail "Checksum verification failed. Expected: $expectedHash, Got: $actualHash"
|
|
}
|
|
Write-Ok "Checksum verified"
|
|
} else {
|
|
Write-Warn "Could not find checksum entry for $releaseAsset — skipping verification."
|
|
}
|
|
} catch {
|
|
Write-Warn "Could not download checksums.txt — skipping verification."
|
|
}
|
|
|
|
Expand-Archive -Path (Join-Path $tmpDir "multica.zip") -DestinationPath $tmpDir -Force
|
|
|
|
$binDir = Join-Path $env:USERPROFILE ".multica\bin"
|
|
if (-not (Test-Path $binDir)) {
|
|
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
|
|
}
|
|
|
|
$exeSrc = Join-Path $tmpDir "multica.exe"
|
|
if (-not (Test-Path $exeSrc)) {
|
|
$exeSrc = Get-ChildItem -Path $tmpDir -Filter "multica.exe" -Recurse | Select-Object -First 1 -ExpandProperty FullName
|
|
}
|
|
if (-not $exeSrc -or -not (Test-Path $exeSrc)) {
|
|
Remove-Item $tmpDir -Recurse -Force
|
|
Write-Fail "multica.exe not found in downloaded archive."
|
|
}
|
|
|
|
Copy-Item $exeSrc (Join-Path $binDir "multica.exe") -Force
|
|
Remove-Item $tmpDir -Recurse -Force
|
|
|
|
Add-ToUserPath $binDir
|
|
Write-Ok "Multica CLI installed to $binDir\multica.exe"
|
|
}
|
|
|
|
function Add-ToUserPath {
|
|
param([string]$Dir)
|
|
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
if ($currentPath -and $currentPath.Split(";") -contains $Dir) {
|
|
return
|
|
}
|
|
$newPath = if ($currentPath) { "$currentPath;$Dir" } else { $Dir }
|
|
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
# Also update current session
|
|
if ($env:Path -notlike "*$Dir*") {
|
|
$env:Path = "$Dir;$env:Path"
|
|
}
|
|
Write-Info "Added $Dir to user PATH (restart your terminal for other sessions to pick it up)."
|
|
}
|
|
|
|
function Install-Cli {
|
|
if (Test-CommandExists "multica") {
|
|
$currentVer = Get-InstalledCliVersion
|
|
$latestVer = Get-LatestVersion
|
|
|
|
$currentCmp = if ($currentVer) { $currentVer -replace '^v','' } else { $null }
|
|
$latestCmp = if ($latestVer) { $latestVer -replace '^v','' } else { $null }
|
|
|
|
$isUpToDate = $currentCmp -and -not $latestCmp
|
|
if (-not $isUpToDate) {
|
|
try {
|
|
$isUpToDate = $currentCmp -and $latestCmp -and ([System.Version]$currentCmp -ge [System.Version]$latestCmp)
|
|
} catch {
|
|
$isUpToDate = $currentCmp -and $latestCmp -and ($currentCmp -eq $latestCmp)
|
|
}
|
|
}
|
|
|
|
if ($isUpToDate) {
|
|
Write-Ok "Multica CLI is up to date ($currentVer)"
|
|
return
|
|
}
|
|
|
|
Write-Info "Multica CLI $currentVer installed, latest is $latestVer - upgrading..."
|
|
Install-CliBinary
|
|
|
|
$newVer = Get-InstalledCliVersion
|
|
Write-Ok "Multica CLI upgraded ($currentVer -> $newVer)"
|
|
return
|
|
}
|
|
|
|
Install-CliBinary
|
|
|
|
if (-not (Test-CommandExists "multica")) {
|
|
Write-Fail "CLI installed but 'multica' not found on PATH. Restart your terminal and try again."
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Docker check
|
|
# ---------------------------------------------------------------------------
|
|
function Test-Docker {
|
|
if (-not (Test-CommandExists "docker")) {
|
|
Write-Fail @"
|
|
Docker is not installed. Multica self-hosting requires Docker and Docker Compose.
|
|
|
|
Install Docker Desktop for Windows:
|
|
https://docs.docker.com/desktop/install/windows-install/
|
|
|
|
After installing Docker, re-run this script with `$env:MULTICA_MODE="local"`.
|
|
"@
|
|
}
|
|
|
|
try {
|
|
docker info 2>$null | Out-Null
|
|
} catch {
|
|
Write-Fail "Docker is installed but not running. Please start Docker Desktop and re-run this script."
|
|
}
|
|
|
|
Write-Ok "Docker is available"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server setup (self-host / local)
|
|
# ---------------------------------------------------------------------------
|
|
function Install-Server {
|
|
Write-Info "Setting up Multica server..."
|
|
$serverRef = Get-SelfHostRef
|
|
Write-Info "Using self-host assets from $serverRef..."
|
|
|
|
if (Test-Path (Join-Path $InstallDir ".git")) {
|
|
Write-Info "Updating existing installation at $InstallDir..."
|
|
Write-Warn "Any local changes in $InstallDir will be overwritten."
|
|
} else {
|
|
Write-Info "Cloning Multica repository..."
|
|
if (-not (Test-CommandExists "git")) {
|
|
Write-Fail "Git is not installed. Please install git and re-run."
|
|
}
|
|
if (Test-Path $InstallDir) {
|
|
Write-Warn "Removing incomplete installation at $InstallDir..."
|
|
Remove-Item $InstallDir -Recurse -Force
|
|
}
|
|
$parentDir = Split-Path $InstallDir -Parent
|
|
if (-not (Test-Path $parentDir)) {
|
|
New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
|
|
}
|
|
git clone --depth 1 $RepoUrl $InstallDir
|
|
}
|
|
|
|
Push-Location $InstallDir
|
|
Checkout-ServerRef $serverRef
|
|
Write-Ok "Repository ready at $InstallDir ($serverRef)"
|
|
|
|
if (-not (Test-Path ".env")) {
|
|
Write-Info "Creating .env with random secrets..."
|
|
Copy-Item ".env.example" ".env"
|
|
$jwt = New-RandomHex 32
|
|
$pgpass = New-RandomHex 24
|
|
$content = Get-Content ".env"
|
|
$content = $content -replace '^JWT_SECRET=.*', "JWT_SECRET=$jwt"
|
|
$content = $content -replace '^POSTGRES_PASSWORD=.*', "POSTGRES_PASSWORD=$pgpass"
|
|
$content = $content -replace '^(DATABASE_URL=postgres://[^:]+:)[^@]*(@.*)', "`${1}$pgpass`${2}"
|
|
$content | Set-Content ".env"
|
|
Write-Ok "Generated .env with random JWT_SECRET and POSTGRES_PASSWORD"
|
|
} else {
|
|
Write-Ok "Using existing .env"
|
|
}
|
|
|
|
Write-Info "Pulling official Multica images..."
|
|
Pull-OfficialSelfHostImages
|
|
Write-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.
|
|
$script:SelfHostBackendPort = Get-ComposePublishedPort -Service "backend" -ContainerPort 8080
|
|
if (-not $script:SelfHostBackendPort) {
|
|
Write-Fail "Started the stack but could not read the backend host port from Docker Compose.`n Check it with: cd $InstallDir; docker compose -f docker-compose.selfhost.yml ps"
|
|
}
|
|
$script:SelfHostFrontendPort = Get-ComposePublishedPort -Service "frontend" -ContainerPort 3000
|
|
if (-not $script:SelfHostFrontendPort) {
|
|
Write-Fail "Started the stack but could not read the frontend host port from Docker Compose.`n Check it with: cd $InstallDir; docker compose -f docker-compose.selfhost.yml ps"
|
|
}
|
|
|
|
Write-Info "Waiting for backend to be ready..."
|
|
$ready = $false
|
|
for ($i = 1; $i -le 45; $i++) {
|
|
try {
|
|
$null = Invoke-WebRequest -Uri "http://localhost:$($script:SelfHostBackendPort)/health" -UseBasicParsing -TimeoutSec 2
|
|
$ready = $true
|
|
break
|
|
} catch {
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
}
|
|
|
|
if ($ready) {
|
|
Write-Ok "Multica server is running"
|
|
} else {
|
|
Write-Warn "Server is still starting. Check logs with:"
|
|
Write-Host " cd $InstallDir; docker compose -f docker-compose.selfhost.yml logs"
|
|
}
|
|
|
|
Pop-Location
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main: Default mode (cloud)
|
|
# ---------------------------------------------------------------------------
|
|
function Start-DefaultInstall {
|
|
Write-Host ""
|
|
Write-Host " Multica - Installer" -ForegroundColor White
|
|
Write-Host ""
|
|
|
|
Install-Cli
|
|
|
|
Write-Host ""
|
|
Write-Host " ============================================" -ForegroundColor Green
|
|
Write-Host " [OK] Multica CLI is ready!" -ForegroundColor Green
|
|
Write-Host " ============================================" -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host " Next: configure your environment"
|
|
Write-Host ""
|
|
Write-Host " multica setup " -NoNewline; Write-Host "# Connect to Multica Cloud (multica.ai)" -ForegroundColor DarkGray
|
|
Write-Host " multica setup self-host " -NoNewline; Write-Host "# Connect to a self-hosted server" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
Write-Host " Self-hosting? Install the server first:"
|
|
Write-Host ' $env:MULTICA_MODE="with-server"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex'
|
|
Write-Host ""
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main: Local mode (self-host)
|
|
# ---------------------------------------------------------------------------
|
|
function Start-LocalInstall {
|
|
Write-Host ""
|
|
Write-Host " Multica - Self-Host Installer" -ForegroundColor White
|
|
Write-Host " Provisioning server infrastructure + installing CLI"
|
|
Write-Host ""
|
|
|
|
Test-Docker
|
|
Install-Server
|
|
Install-Cli
|
|
|
|
Write-Host ""
|
|
Write-Host " ============================================" -ForegroundColor Green
|
|
Write-Host " [OK] Multica server is running and CLI is ready!" -ForegroundColor Green
|
|
Write-Host " ============================================" -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host " Frontend: http://localhost:$($script:SelfHostFrontendPort)"
|
|
Write-Host " Backend: http://localhost:$($script:SelfHostBackendPort)"
|
|
Write-Host " Server at: $InstallDir"
|
|
Write-Host ""
|
|
Write-Host " Next: configure your CLI to connect"
|
|
Write-Host ""
|
|
Write-Host " multica setup self-host " -NoNewline; Write-Host "# Configure + authenticate + start daemon" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
Write-Host " Login: configure RESEND_API_KEY in .env for email codes,"
|
|
Write-Host " or read the generated code from backend logs when Resend is unset."
|
|
Write-Host ""
|
|
Write-Host " To stop all services:"
|
|
Write-Host ' $env:MULTICA_MODE="stop"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex'
|
|
Write-Host ""
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stop: shut down a self-hosted installation
|
|
# ---------------------------------------------------------------------------
|
|
function Start-Stop {
|
|
Write-Host ""
|
|
Write-Info "Stopping Multica services..."
|
|
|
|
if (Test-Path $InstallDir) {
|
|
Push-Location $InstallDir
|
|
if (Test-Path "docker-compose.selfhost.yml") {
|
|
docker compose -f docker-compose.selfhost.yml down
|
|
Write-Ok "Docker services stopped"
|
|
} else {
|
|
Write-Warn "No docker-compose.selfhost.yml found at $InstallDir"
|
|
}
|
|
Pop-Location
|
|
} else {
|
|
Write-Warn "No Multica installation found at $InstallDir"
|
|
}
|
|
|
|
if (Test-CommandExists "multica") {
|
|
try {
|
|
multica daemon stop 2>$null
|
|
Write-Ok "Daemon stopped"
|
|
} catch {}
|
|
}
|
|
|
|
Write-Host ""
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
$mode = if ($env:MULTICA_MODE) { $env:MULTICA_MODE.ToLower() } else { "default" }
|
|
|
|
switch ($mode) {
|
|
"with-server" { Start-LocalInstall }
|
|
"local" { Start-LocalInstall } # backwards compat alias
|
|
"stop" { Start-Stop }
|
|
default { Start-DefaultInstall }
|
|
}
|