# Multica installer for Windows - installs or upgrades the Multica CLI. # # Install / upgrade CLI: # irm https://work.vibesolutions.vn/releases/install.ps1 | iex # # After installation, run `multica setup` to configure your environment. # $ErrorActionPreference = "Stop" # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # Every download comes from this release host. Override with the environment # variable of the same name (e.g. for a mirror or a test server). $ReleasesUrl = if ($env:MULTICA_RELEASES_URL) { $env:MULTICA_RELEASES_URL } else { "https://work.vibesolutions.vn/releases" } $ReleasesUrl = $ReleasesUrl.TrimEnd('/') # Web app that serves the releases (used for user-facing links). $AppUrl = $ReleasesUrl -replace '/releases$', '' # Windows PowerShell 5.1 may default to TLS 1.0/1.1; the release host needs 1.2. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} # --------------------------------------------------------------------------- # 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) } # Downloads a small text file and returns its content as a string. function Get-RemoteText { param([string]$Uri) $response = Invoke-WebRequest -Uri $Uri -UseBasicParsing -ErrorAction Stop if ($response.Content -is [byte[]]) { return [System.Text.Encoding]::UTF8.GetString($response.Content) } return [string]$response.Content } # Latest release tag (e.g. v0.6.0-vibe.1), or $null when it cannot be read. # MULTICA_VERSION pins a specific tag instead. function Get-LatestVersion { if ($env:MULTICA_VERSION) { return $env:MULTICA_VERSION } try { $text = Get-RemoteText -Uri "$ReleasesUrl/latest.txt" $tag = (($text -split "`r?`n") | Where-Object { $_.Trim() } | Select-Object -First 1) if ($tag) { return $tag.Trim() } } catch {} return $null } 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+)+(?:-[0-9A-Za-z.]+)?)') { $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 $ReleasesUrl ..." 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 from $ReleasesUrl/latest.txt. Check your network connection." } $version = $latest.TrimStart('v') $releaseAsset = "multica-cli-$version-windows-$arch.zip" $baseUrl = "$ReleasesUrl/download/$latest" $url = "$baseUrl/$releaseAsset" $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 $checksumContent = $null try { $checksumContent = Get-RemoteText -Uri "$baseUrl/checksums.txt" } catch { Write-Warn "Could not download checksums.txt - skipping verification." } if ($null -ne $checksumContent) { $zipFile = Join-Path $tmpDir "multica.zip" $actualHash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash.ToLower() $expectedLine = ($checksumContent -split "`r?`n") | Where-Object { $_ -match ('\s\*?' + [regex]::Escape($releaseAsset) + '\s*$') } | Select-Object -First 1 if ($expectedLine) { $expectedHash = ($expectedLine.Trim() -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." } } 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 { # Tags such as 0.6.0-vibe.1 are not System.Version values. $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." } } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- 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 $AppUrl" -ForegroundColor DarkGray Write-Host "" } # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- $mode = if ($env:MULTICA_MODE) { $env:MULTICA_MODE.ToLower() } else { "default" } switch ($mode) { { $_ -in @("with-server", "local", "stop") } { Write-Fail "MULTICA_MODE=$mode is not supported: the Multica server is installed and managed by your administrator. Unset MULTICA_MODE to install the CLI." } default { Start-DefaultInstall } }