# Configuration-only bootstrap. Service lifecycle and database migration are manual. [CmdletBinding()] param( [string]$InstallDir = "", [string]$Version = "4.0.0" ) $ErrorActionPreference = "Stop" $Repository = "YuanYeYouTao/Yuki" # This source installer targets the matching future Yuki release artifacts. # Published releases retain their own installers and immutable image names. $BotImage = "ghcr.io/yuanyeyoutao/yuki" function Fail([string]$Message) { throw $Message } if ($Version -notmatch '^\d+\.\d+\.\d+$') { Fail "Version must use X.Y.Z." } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Fail "Docker is not installed." } & docker compose version *> $null if ($LASTEXITCODE -ne 0) { Fail "The Docker Compose CLI plugin is not available." } & docker info *> $null if ($LASTEXITCODE -ne 0) { Fail "Docker Engine is not running." } $Architecture = (& docker info --format '{{.Architecture}}').Trim() $DockerOs = (& docker info --format '{{.OSType}}').Trim() if ($DockerOs -ne 'linux') { Fail "Docker Desktop must be running Linux containers." } $Platform = switch ($Architecture) { { $_ -in @('amd64', 'x86_64') } { 'linux/amd64' } { $_ -in @('arm64', 'aarch64') } { 'linux/arm64' } default { Fail "Yuki $Version supports linux/amd64 and linux/arm64; Docker reports $Architecture." } } if (-not $InstallDir) { if ((Test-Path -LiteralPath "docker-compose.yml") -and (Test-Path -LiteralPath ".env.example")) { $InstallDir = (Get-Location).Path } else { $InstallDir = Join-Path (Get-Location).Path "yuki" } } $InstallDir = [System.IO.Path]::GetFullPath($InstallDir) [System.IO.Directory]::CreateDirectory($InstallDir) | Out-Null $WriteProbe = Join-Path $InstallDir (".yuki-write-test-" + [guid]::NewGuid()) try { [System.IO.File]::WriteAllText($WriteProbe, "write-test") } catch { Fail "Installation directory is not writable." } finally { if (Test-Path -LiteralPath $WriteProbe) { Remove-Item -LiteralPath $WriteProbe -Force } } $ComposePath = Join-Path $InstallDir "docker-compose.yml" $EnvTemplatePath = Join-Path $InstallDir ".env.example" $Existing = (Test-Path -LiteralPath $ComposePath) -and (Test-Path -LiteralPath $EnvTemplatePath) if (-not $Existing -and (Get-ChildItem -LiteralPath $InstallDir -Force | Select-Object -First 1)) { Fail "Installation directory is not empty and is not a Yuki deployment." } $TemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) $Temporary = Join-Path $TemporaryRoot ("yuki-setup-" + [guid]::NewGuid()) [System.IO.Directory]::CreateDirectory($Temporary) | Out-Null try { if (-not $Existing) { $Base = "https://github.com/$Repository/releases/download/v$Version" $ArchiveName = "yuki-$Version-deploy.zip" $Archive = Join-Path $Temporary $ArchiveName $Checksums = Join-Path $Temporary "SHA256SUMS" Invoke-WebRequest -Uri "$Base/$ArchiveName" -OutFile $Archive Invoke-WebRequest -Uri "$Base/SHA256SUMS" -OutFile $Checksums $ChecksumLine = Get-Content -LiteralPath $Checksums | Where-Object { $_ -match "^[0-9a-fA-F]{64}\s+$([regex]::Escape($ArchiveName))$" } | Select-Object -First 1 if (-not $ChecksumLine) { Fail "Release checksum does not list $ArchiveName." } $Expected = ($ChecksumLine -split '\s+')[0].ToLowerInvariant() $Actual = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() if ($Expected -ne $Actual) { Fail "Release archive checksum mismatch." } Expand-Archive -LiteralPath $Archive -DestinationPath $Temporary $Source = Join-Path $Temporary "yuki-$Version-deploy" if (-not (Test-Path -LiteralPath $Source -PathType Container)) { Fail "Release archive layout is invalid." } if (-not $Existing) { Copy-Item -Path (Join-Path $Source '*') -Destination $InstallDir -Recurse -Force Get-ChildItem -LiteralPath $Source -Force | Where-Object Name -Like '.*' | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $InstallDir -Recurse -Force } } } $Image = "${BotImage}:$Version" Write-Host "Pulling $Image for $Platform" -ForegroundColor Cyan & docker pull --platform $Platform $Image if ($LASTEXITCODE -ne 0) { Fail "Unable to pull $Image." } & docker run --rm -it ` --platform $Platform ` --entrypoint yuki ` --volume "${InstallDir}:/deploy" ` --workdir /deploy ` $Image setup --deployment-root /deploy if ($LASTEXITCODE -ne 0) { Fail "Guided setup did not complete." } if ($env:OS -eq "Windows_NT") { $Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $PrivateTargets = @( @{ Path = (Join-Path $InstallDir ".env") Permission = "${Identity}:(F)" Recurse = $false }, @{ Path = (Join-Path $InstallDir ".yuki\backups") Permission = "${Identity}:(OI)(CI)F" Recurse = $true }, @{ Path = (Join-Path $InstallDir "character") Permission = "${Identity}:(OI)(CI)F" Recurse = $true } ) foreach ($Target in $PrivateTargets) { if (Test-Path -LiteralPath $Target.Path) { $AclArguments = @($Target.Path, "/inheritance:r", "/grant:r", $Target.Permission) if ($Target.Recurse) { $AclArguments += @("/T", "/C") } & icacls @AclArguments *> $null if ($LASTEXITCODE -ne 0) { Fail "Unable to restrict configuration ACLs." } } } } Write-Host "Configuration saved. No services were stopped or started and no database was upgraded." -ForegroundColor Green Write-Host "Review $InstallDir/Yuki-$Version-Upgrade.md before starting or upgrading the deployment." Write-Host "Upgrade guide: https://github.com/$Repository/blob/v$Version/docs/upgrade-$Version.md" Write-Host "Path file tools and terminals require the Linux Docker host Manager; Compose does not install it." Write-Host "After preparing the persistent-environment prerequisites, explicitly run on that Linux host:" Write-Host "sudo sh ./deploy/sandbox/install-manager.sh --deployment-root /absolute/deployment/path --bot-container BOT_CONTAINER" Write-Host "Use the Linux host deployment root; a Windows client path is not the Docker host path." Write-Host "Then check: docker compose exec --user 10001:10001 bot yuki setup environment-check" } finally { $ResolvedTemporary = [System.IO.Path]::GetFullPath($Temporary) if ([System.IO.Path]::GetDirectoryName($ResolvedTemporary).TrimEnd([System.IO.Path]::DirectorySeparatorChar) -ne $TemporaryRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar)) { Fail "Unexpected temporary directory; cleanup refused." } if (Test-Path -LiteralPath $ResolvedTemporary) { Remove-Item -LiteralPath $ResolvedTemporary -Recurse -Force } }