# Lanjunex WorkBuddy setup. Paste into Windows PowerShell; no administrator required. # Only free GET requests are made. Model generation is never triggered here. & { $ErrorActionPreference = 'Stop' $key = $null $apiRoot = 'https://api.lanjunex.com' $modelIds = @() $endpoint = "$apiRoot/v1/chat/completions" $stamp = [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [Guid]::NewGuid().ToString('N').Substring(0,8) $plans = @() $revision = '20261026-r8' $stage = 'start' $currentFolder = 'none' $resultStatus = 'not-started' $statusCode = 'NOT_STARTED' $httpStatus = 0 $rollbackState = 'not-needed' $replaceOldKey = $null function Stop-Setup([string]$code, [string]$message) { $failure = [InvalidOperationException]::new($message) $failure.Data['SetupCode'] = $code $failure.Data['SafeMessage'] = $message throw $failure } function Wait-WorkBuddyExit { while (Get-Process -Name 'WorkBuddy','workbuddy' -ErrorAction SilentlyContinue) { Write-Host 'WorkBuddy is still running. Right-click its system-tray icon and choose Exit. No process will be killed.' -ForegroundColor Yellow $answer = Read-Host 'WorkBuddy: press Enter after exiting to check again, or Q to cancel' if ([string]$answer -ieq 'Q') { Stop-Setup 'USER_CANCELLED' 'Setup cancelled. Exit WorkBuddy from the system tray before running setup again.' } } } function Show-SafeDiagnostic { $synced = 0; $skipped = 0; $switched = 0 foreach ($item in $plans) { $synced += @($item.ids).Count; $skipped += @($item.conflicts).Count; $switched += @($item.switched).Count } if ($resultStatus -notin @('complete', 'partial')) { $synced = 0; $switched = 0 if ($rollbackState -eq 'incomplete') { $synced = 'unknown'; $switched = 'unknown' } } $safeOS = if ($env:OS -eq 'Windows_NT') { 'Windows' } else { 'non-Windows-simulation' } $summary = @( 'WORKBUDDY DIAGNOSTIC BEGIN', ('revision=' + $revision), ('os=' + $safeOS), ('powershell=' + $PSVersionTable.PSVersion.ToString()), ('status=' + $resultStatus), ('code=' + $statusCode), ('stage=' + $stage), ('folder=' + $currentFolder), ('http=' + $httpStatus), ('compatible_models=' + $modelIds.Count), ('synced_entries=' + $synced), ('skipped_entries=' + $skipped), ('switched_entries=' + $switched), ('rollback=' + $rollbackState), 'WORKBUDDY DIAGNOSTIC END' ) -join [Environment]::NewLine Write-Host $summary Write-Host 'Only the diagnostic block above is safe to share. Do not share models.json or its backups.' if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) { try { $copyChoice = Read-Host 'Copy diagnostic summary: C to copy, or Enter to skip' if ([string]$copyChoice -ieq 'C') { Set-Clipboard -Value $summary -ErrorAction Stop Write-Host 'Safe diagnostic summary copied.' } } catch { Write-Host 'Clipboard is unavailable. Select and copy the diagnostic block above instead.' -ForegroundColor Yellow } } } function Protect-LocalFile([string]$path) { if ($env:OS -eq 'Windows_NT') { $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User $acl = [System.Security.AccessControl.FileSecurity]::new() $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'Allow')) # Persist only the modified DACL. Do not rewrite owner/group/audit # sections through Set-Acl (which can require SeSecurityPrivilege). $file = [IO.FileInfo]::new($path) if ($PSVersionTable.PSVersion.Major -eq 5) { $file.SetAccessControl($acl) } else { [IO.FileSystemAclExtensions]::SetAccessControl($file, $acl) } } } function Test-InstallerModelName($model) { # Name is optional. Keep legacy installer entries recognizable after # migrating to ID-only labels, without taking over custom user labels. return [string]::IsNullOrEmpty([string]$model.name) -or [string]::Equals([string]$model.name, [string]$model.id, [StringComparison]::Ordinal) -or $model.name -like 'Lanjunex - *' } function Test-ReasoningStrengthSupport([string]$modelId) { # Exact, reviewed effort-control compatibility. Native thinking alone # does not mean this Chat route supports an adjustable effort control. switch -CaseSensitive ($modelId) { 'gpt-5.6-sol' { return $true } 'gpt-5.6-terra' { return $true } 'gpt-5.6-luna' { return $true } 'gpt-6-sol' { return $true } 'gpt-6-luna' { return $true } 'gpt-6.1-sol' { return $true } 'gpt-6-astra' { return $true } 'claude-fable-5' { return $true } 'claude-fable-5-1' { return $true } 'claude-opus-4-7' { return $true } 'claude-opus-4-8' { return $true } 'claude-opus-5' { return $true } 'claude-sonnet-5' { return $true } 'claude-haiku-4-5' { return $true } 'claude-haiku-4-5-20251001' { return $true } 'kimi-k3' { return $true } 'claude-opus-5-5' { return $true } 'claude-sonnet-5-5' { return $true } 'claude-haiku-5-5' { return $true } 'deepseek-v4.1-flash-1017' { return $true } 'glm-5.3' { return $true } 'qwen3.8-flash' { return $true } default { return $false } } } function Assert-ModelConfiguration($document, [string[]]$expectedIds, [string]$expectedUrl, [string]$expectedKey, [string]$stage) { foreach ($expectedModelId in $expectedIds) { $idCount = 0 $urlCount = 0 $keyCount = 0 $reasoningCount = 0 foreach ($entry in @($document.models)) { if ([string]::Equals([string]$entry.id, $expectedModelId, [StringComparison]::Ordinal)) { $idCount++ if ([string]::Equals([string]$entry.url, $expectedUrl, [StringComparison]::Ordinal)) { $urlCount++ } if ([string]::Equals([string]$entry.apiKey, $expectedKey, [StringComparison]::Ordinal)) { $keyCount++ } if ($entry.supportsReasoning -is [bool] -and $entry.supportsReasoning -eq (Test-ReasoningStrengthSupport $expectedModelId)) { $reasoningCount++ } } } if ($idCount -ne 1 -or $urlCount -ne 1 -or $keyCount -ne 1 -or $reasoningCount -ne 1) { Stop-Setup 'CONFIG_VALIDATION' "Configuration validation failed [$stage] (id=$idCount, url=$urlCount, key=$keyCount, reasoning=$reasoningCount). Key values are never displayed." } } } try { $runtimeVersion = $PSVersionTable.PSVersion if ($runtimeVersion.Major -lt 5 -or ($runtimeVersion.Major -eq 5 -and $runtimeVersion.Minor -lt 1)) { Stop-Setup 'UNSUPPORTED_POWERSHELL' 'This Windows installer requires PowerShell 5.1 or PowerShell 7. No configuration was changed.' } Write-Host ('Detected PowerShell ' + $runtimeVersion.ToString() + '. PowerShell 5.1 and 7 use the same safe setup.') [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 Write-Host 'Lanjunex -> WorkBuddy (automatic model sync / Windows permission fix / model-specific reasoning 20261026-r8)' Write-Host 'Exit WorkBuddy from the system tray first. Existing models are preserved.' $stage = 'close-app'; $statusCode = 'CHECK_APP' Wait-WorkBuddyExit $stage = 'read-key'; $statusCode = 'READ_KEY' $secureKey = Read-Host 'Paste your EXISTING Lanjunex API Key (hidden)' -AsSecureString $key = [System.Net.NetworkCredential]::new('', $secureKey).Password.Trim() if ([string]::IsNullOrWhiteSpace($key) -or $key -match '\s') { Stop-Setup 'INVALID_KEY_INPUT' 'Empty or invalid API Key. No configuration was changed.' } $stage = 'catalog'; $statusCode = 'READ_CATALOG' Write-Host '[1/3] Checking this Key and its compatible model list (free GET only)...' # Check Key access without sending a prompt or spending tokens. try { $catalog = Invoke-RestMethod -Method Get -Uri "$apiRoot/v1/models?endpoint=chat_completions" -Headers @{ Authorization = "Bearer $key" } -TimeoutSec 30 $httpStatus = 200 } catch { $httpStatus = 0 if ($_.Exception.Response) { try { $httpStatus = [int]$_.Exception.Response.StatusCode } catch {} } if ($httpStatus -eq 401) { Stop-Setup 'HTTP_401' 'The Key was not accepted (401). Check the existing Lanjunex Key. No configuration was changed.' } if ($httpStatus -eq 403) { Stop-Setup 'HTTP_403' 'This Key is currently restricted (403). Check its status and restrictions. No configuration was changed.' } if ($httpStatus -eq 429) { Stop-Setup 'HTTP_429' 'The free model-list endpoint is rate limited (429). Retry later. No configuration was changed.' } Stop-Setup 'CATALOG_UNAVAILABLE' "Free compatible-model lookup failed (HTTP $httpStatus). Check the network or service status. No configuration was changed." } # An explicit marker prevents an older gateway from ignoring the query # and returning native-only models that WorkBuddy cannot call. if ($catalog.endpoint -cne 'chat_completions' -or $catalog.data -isnot [Array]) { Stop-Setup 'UNVERIFIED_CATALOG' 'The gateway did not return a verified Chat-compatible model list. No configuration was changed.' } $seenIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($item in $catalog.data) { $id = $item.id if ($id -isnot [string] -or $id -notmatch '^[^\s\p{C}]{1,256}$') { Stop-Setup 'INVALID_MODEL_ID' 'The gateway returned an invalid model ID. No configuration was changed.' } if ($seenIds.Add($id)) { $modelIds += $id } } if ($modelIds.Count -eq 0) { if ($catalog.reason -eq 'client_restricted') { Stop-Setup 'CLIENT_RESTRICTED' 'This Key scope restricts this client. No model or Key settings were changed.' } if ($catalog.reason -eq 'unsupported_catalog_scope') { Stop-Setup 'UNSUPPORTED_CATALOG_SCOPE' 'This Key scope does not publish a verified WorkBuddy-compatible catalog. Existing settings were kept.' } if ($catalog.reason -eq 'fallback_catalog_unverified') { Stop-Setup 'FALLBACK_CATALOG_UNVERIFIED' 'The fallback scope does not publish a verified WorkBuddy-compatible catalog. Existing settings were kept.' } Stop-Setup 'NO_COMPATIBLE_MODELS' 'No WorkBuddy-compatible models are currently published for this Key. Existing configuration and Key settings were kept.' } $modelIds = @($modelIds | Sort-Object) Write-Host ("Compatible models for this Key: " + $modelIds.Count) Write-Host ($modelIds -join ', ') Write-Host 'Native-only Messages/Responses models are not imported. Key permissions and prices are unchanged.' $stage = 'prepare-config'; $statusCode = 'PREPARE_CONFIG' # WorkBuddy versions use either directory. Keep both compatible user files in sync. foreach ($folder in @('.workbuddy', '.codebuddy')) { $currentFolder = $folder $dir = Join-Path $env:USERPROFILE $folder $path = Join-Path $dir 'models.json' if ((Test-Path -LiteralPath $dir) -and ((Get-Item -LiteralPath $dir -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Stop-Setup 'LINKED_CONFIG_PATH' ('Refusing a linked configuration directory: ' + $folder + '. Original files were kept.') } $exists = Test-Path -LiteralPath $path $raw = $null if ($exists) { $item = Get-Item -LiteralPath $path -Force if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { Stop-Setup 'INVALID_CONFIG_PATH' ('Not a regular configuration file in ' + $folder + '. Original files were kept.') } $stage = 'read-config'; $statusCode = 'LOCAL_FILE_ACCESS' $raw = [IO.File]::ReadAllText($path) if ([string]::IsNullOrWhiteSpace($raw)) { $config = [PSCustomObject]@{} } else { try { $config = ConvertFrom-Json -InputObject $raw } catch { Stop-Setup 'INVALID_JSON' ('Invalid JSON in ' + $folder + '. Original file kept; fix it before running setup.') } } if ($null -eq $config -or $config -isnot [PSCustomObject]) { Stop-Setup 'INVALID_JSON_SHAPE' ('Expected a JSON object in ' + $folder + '. Original file kept.') } } else { $config = [PSCustomObject]@{} } if ($config.PSObject.Properties['models'] -and $config.models -isnot [Array]) { Stop-Setup 'INVALID_MODEL_LIST' ('models must be an array in ' + $folder + '. Original file kept.') } if ($config.PSObject.Properties['availableModels'] -and $config.availableModels -isnot [Array]) { Stop-Setup 'INVALID_VISIBLE_LIST' ('availableModels must be an array in ' + $folder + '. Original file kept.') } $models = @() if ($config.PSObject.Properties['models']) { $models = @($config.models) } foreach ($model in $models) { if ($null -eq $model -or $model -isnot [PSCustomObject]) { Stop-Setup 'INVALID_MODEL_ENTRY' ('Invalid model entry in ' + $folder + '. Original file kept.') } } $staleIds = @() $keptModels = @() foreach ($model in $models) { $owned = $model.url -ceq $endpoint -and $model.apiKey -ceq $key -and (Test-InstallerModelName $model) if ($owned -and $modelIds -cnotcontains $model.id) { $staleIds += $model.id } else { $keptModels += $model } } $models = $keptModels $syncedIds = @() $conflicts = @() $switched = @() foreach ($id in $modelIds) { $existingModelIndices = @() for ($modelIndex = 0; $modelIndex -lt $models.Count; $modelIndex++) { if ([string]::Equals([string]$models[$modelIndex].id, $id, [StringComparison]::Ordinal)) { $existingModelIndices += $modelIndex } } if ($existingModelIndices.Count -gt 1) { $conflicts += $id; continue } $modelValues = [ordered]@{} if ($existingModelIndices.Count -eq 1) { $model = $models[$existingModelIndices[0]] if ($model.url -cne $endpoint -or -not (Test-InstallerModelName $model)) { $conflicts += $id; continue } if ($model.apiKey -cne $key) { if ([string]::IsNullOrWhiteSpace([string]$model.apiKey)) { $conflicts += $id; continue } if ($null -eq $replaceOldKey) { Write-Host 'Some matching models use a different Lanjunex Key. No Key values are displayed.' -ForegroundColor Yellow Write-Host 'R switches matching installer-managed model IDs to the Key you just entered, in both folders. Other providers, custom labels and non-overlapping models stay unchanged. Originals will be backed up.' $replaceChoice = Read-Host 'Replace matching old Key entries? R to switch; Enter or K to keep the old entries' $replaceOldKey = [string]$replaceChoice -ieq 'R' } if (-not $replaceOldKey) { $conflicts += $id; continue } $switched += $id } foreach ($property in $model.PSObject.Properties) { $modelValues[$property.Name] = $property.Value } } # Construct the complete dictionary BEFORE inserting it. Do not # rely on Add-Member changes propagating through array wrappers. $modelValues['id'] = [string]$id # Let WorkBuddy display the ID once; an extra name can render as name:id. $modelValues.Remove('name') $modelValues['vendor'] = 'OpenAI' $modelValues['url'] = [string]$endpoint $modelValues['apiKey'] = [string]$key $defaults = @{ supportsToolCall = $true; supportsImages = $false } foreach ($field in $defaults.Keys) { if (-not $modelValues.Contains($field)) { $modelValues[$field] = $defaults[$field] } } # Refresh old installer defaults too; do not leave known-supported IDs off. $modelValues['supportsReasoning'] = Test-ReasoningStrengthSupport $id if ($existingModelIndices.Count -eq 1) { $models[$existingModelIndices[0]] = $modelValues } else { $models += $modelValues } $syncedIds += $id } if ($syncedIds.Count -eq 0) { Stop-Setup 'MODEL_CONFLICT' ('No compatible models can be synced in ' + $folder + '. Existing entries keep their original Key/provider. No configuration was changed.') } $configValues = [ordered]@{} foreach ($property in $config.PSObject.Properties) { $configValues[$property.Name] = $property.Value } $configValues['models'] = [object[]]$models # An absent/empty whitelist means all built-in models stay visible. if ($config.PSObject.Properties['availableModels'] -and @($config.availableModels).Count -gt 0) { $visible = @($config.availableModels | Where-Object { $staleIds -cnotcontains $_ }) foreach ($id in $syncedIds) { if ($visible -cnotcontains $id) { $visible += $id } } $configValues['availableModels'] = [object[]]$visible } $stage = 'build-config'; $statusCode = 'CONFIG_VALIDATION' $json = ConvertTo-Json -InputObject $configValues -Depth 100 -WarningAction Stop $roundTrip = ConvertFrom-Json -InputObject $json Assert-ModelConfiguration $roundTrip ([string[]]$syncedIds) $endpoint $key ('before-write/' + $folder) $plans += [PSCustomObject]@{ dir = $dir; path = $path; existed = $exists; raw = $raw; json = $json; ids = $syncedIds; conflicts = $conflicts; switched = $switched; removed = $staleIds; backup = "$path.lanjunex-$stamp.bak"; temp = "$path.lanjunex-$stamp.tmp"; changed = $false } } # Prepare and protect BOTH temporary files before replacing either original. Write-Host '[2/3] Checking both folders, temporary-file permissions and saved JSON...' foreach ($plan in $plans) { $currentFolder = [IO.Path]::GetFileName($plan.dir) $stage = 'preflight-directory'; $statusCode = 'LOCAL_WRITE_ERROR' $null = [IO.Directory]::CreateDirectory($plan.dir) if ($plan.existed) { if ((Get-Item -LiteralPath $plan.path -Force).IsReadOnly) { Stop-Setup 'READ_ONLY_CONFIG' ('Configuration is read-only in ' + $currentFolder + '. Original files were not changed.') } $probe = [IO.File]::Open($plan.path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read) $probe.Close() } $stream = [IO.File]::Open($plan.temp, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) $stream.Close() $stage = 'preflight-permissions'; $statusCode = 'FILE_PERMISSIONS' Protect-LocalFile $plan.temp $stage = 'preflight-json'; $statusCode = 'CONFIG_VALIDATION' [IO.File]::WriteAllText($plan.temp, $plan.json, [Text.UTF8Encoding]::new($false)) $prepared = [IO.File]::ReadAllText($plan.temp) if (-not [string]::Equals($prepared, $plan.json, [StringComparison]::Ordinal)) { Stop-Setup 'CONFIG_VALIDATION' 'Temporary configuration read-back failed. Original files were not changed.' } Assert-ModelConfiguration (ConvertFrom-Json -InputObject $prepared) ([string[]]$plan.ids) $endpoint $key ('preflight/' + $currentFolder) } $stage = 'close-app-before-write'; $statusCode = 'CHECK_APP'; $currentFolder = 'both' Wait-WorkBuddyExit # Check both originals once more before committing either file. foreach ($plan in $plans) { if ($plan.existed -and [IO.File]::ReadAllText($plan.path) -cne $plan.raw) { Stop-Setup 'CONFIG_CHANGED' 'Configuration changed during setup. Close WorkBuddy and try again.' } if (-not $plan.existed -and (Test-Path -LiteralPath $plan.path)) { Stop-Setup 'CONFIG_CHANGED' 'A configuration file appeared during setup. Close WorkBuddy and try again.' } } Write-Host '[3/3] Saving prepared configurations and verifying both files...' foreach ($plan in $plans) { $currentFolder = [IO.Path]::GetFileName($plan.dir) $stage = 'replace-config'; $statusCode = 'LOCAL_WRITE_ERROR' if ($plan.existed) { if ([IO.File]::ReadAllText($plan.path) -cne $plan.raw) { Stop-Setup 'CONFIG_CHANGED' 'Configuration changed during setup. Close WorkBuddy and try again.' } [IO.File]::Replace($plan.temp, $plan.path, $plan.backup) } else { [IO.File]::Move($plan.temp, $plan.path) } $plan.changed = $true $stage = 'protect-config'; $statusCode = 'FILE_PERMISSIONS' Protect-LocalFile $plan.path if ($plan.existed) { $stage = 'protect-backup'; Protect-LocalFile $plan.backup } } foreach ($plan in $plans) { $currentFolder = [IO.Path]::GetFileName($plan.dir) $stage = 'verify-saved'; $statusCode = 'CONFIG_VALIDATION' $writtenJson = [IO.File]::ReadAllText($plan.path) if (-not [string]::Equals($writtenJson, $plan.json, [StringComparison]::Ordinal)) { Stop-Setup 'CONFIG_CHANGED' 'Saved configuration changed during setup. Original files will be restored.' } $check = ConvertFrom-Json -InputObject $writtenJson Assert-ModelConfiguration $check ([string[]]$plan.ids) $endpoint $key ('read-back/' + $currentFolder) Write-Host ('Synced in ' + $currentFolder + ': ' + ($plan.ids -join ', ')) $reasoningOn = @($plan.ids | Where-Object { Test-ReasoningStrengthSupport $_ }) $reasoningOff = @($plan.ids | Where-Object { -not (Test-ReasoningStrengthSupport $_) }) if ($reasoningOn.Count -gt 0) { Write-Host ('Reasoning strength enabled in ' + $currentFolder + ': ' + ($reasoningOn -join ', ')) } if ($reasoningOff.Count -gt 0) { Write-Host ('Effort control kept off (unsupported or unverified Chat compatibility) in ' + $currentFolder + ': ' + ($reasoningOff -join ', ')) } Write-Host ('Retired entries for this Key removed: ' + $plan.removed.Count + '; matching entries switched to the new Key: ' + $plan.switched.Count) if ($plan.conflicts.Count -gt 0) { Write-Host ('SKIPPED in ' + $currentFolder + ' (still using the original Key/provider): ' + ($plan.conflicts -join ', ')) -ForegroundColor Yellow } if ($plan.existed) { Write-Host ('Original backup: ' + $plan.backup) } } $currentFolder = 'both'; $stage = 'finished' $remainingConflicts = 0 foreach ($plan in $plans) { $remainingConflicts += $plan.conflicts.Count } if ($remainingConflicts -gt 0) { $resultStatus = 'partial'; $statusCode = 'PARTIAL_CONFLICT' Write-Host 'SETUP PARTIAL. Only the listed synced models were updated. Skipped models still use their previous Key/provider.' -ForegroundColor Yellow } else { $resultStatus = 'complete'; $statusCode = 'OK' Write-Host 'SETUP COMPLETE. Restart WorkBuddy and choose a synced model by its model ID.' } Write-Host 'Run this SAME command again to sync added/retired models for the SAME Key.' Write-Host 'Setup made free GET requests only. Sending a message in WorkBuddy uses your Lanjunex balance.' Write-Host 'API Key is stored locally in models.json; do not share that file or its backup.' } catch { $setupError = $_ $knownCode = [string]$setupError.Exception.Data['SetupCode'] if ($knownCode) { $statusCode = $knownCode } $resultStatus = if ($statusCode -eq 'USER_CANCELLED') { 'cancelled' } else { 'stopped' } $changedCount = 0; $restoreFailures = 0 foreach ($plan in $plans) { if ($plan.changed) { $changedCount++ try { if ($plan.existed) { [IO.File]::Copy($plan.backup, $plan.path, $true); Protect-LocalFile $plan.path } else { [IO.File]::Delete($plan.path) } } catch { $restoreFailures++ Write-Host ('Could not fully restore ' + [IO.Path]::GetFileName($plan.dir) + '. Keep its original backup for recovery.') -ForegroundColor Red } } } $rollbackState = if ($changedCount -eq 0) { 'unchanged' } elseif ($restoreFailures -eq 0) { 'restored' } else { 'incomplete' } $safeReason = [string]$setupError.Exception.Data['SafeMessage'] if (-not $safeReason) { $safeReason = 'Check the stage and folder in the safe diagnostic summary. No administrator mode is required; do not disable file protection.' } $publicMessage = 'SETUP STOPPED [' + $statusCode + '] at ' + $stage + ' / ' + $currentFolder + ': ' + $safeReason Write-Host $publicMessage -ForegroundColor Yellow if ($rollbackState -eq 'unchanged') { Write-Host 'Original configuration files were not changed.' } elseif ($rollbackState -eq 'restored') { Write-Host 'Both configurations were restored to their original state.' } else { Write-Host 'Recovery is incomplete. Keep all original backups and contact support with the safe summary.' -ForegroundColor Red } if ($statusCode -ne 'USER_CANCELLED') { throw [InvalidOperationException]::new($publicMessage) } } finally { foreach ($plan in $plans) { try { if ([IO.File]::Exists($plan.temp)) { [IO.File]::Delete($plan.temp) } } catch { Write-Host ('Could not remove a temporary file in ' + [IO.Path]::GetFileName($plan.dir) + '. Do not share it.') -ForegroundColor Yellow } } Show-SafeDiagnostic $key = $null $secureKey = $null $catalog = $null $plans = $null } }