jun-manager.ps1
Rama main · réplica de lectura del repositorio original.
#requires -Version 5.1
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
throw 'The Jun OS manager only runs on Windows.'
}
$script:PowerShellExe = @(
(Join-Path $env:SystemRoot 'Sysnative\WindowsPowerShell\v1.0\powershell.exe'),
(Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe')
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if (-not $script:PowerShellExe) { $script:PowerShellExe = 'powershell.exe' }
# compiled (JunOS.exe) there's no $PSScriptRoot, the folder is
# wherever the exe sits. the installer drops it in her folder,
# next to start.ps1.
$script:IsCompiled = -not $PSCommandPath -or $PSCommandPath.EndsWith('.exe', 'OrdinalIgnoreCase')
$script:Root = if ($script:IsCompiled) {
Split-Path -Parent ([Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)
} else {
$PSScriptRoot
}
if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne [Threading.ApartmentState]::STA) {
if ($script:IsCompiled) { throw 'This build was compiled without -STA. Rebuild with tools/build-installer-exe.ps1 -Manager.' }
Start-Process -FilePath $script:PowerShellExe -WindowStyle Hidden `
-ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', "`"$PSCommandPath`""
exit
}
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
# a second click on the shortcut would start a second window
# racing the first one on start.ps1. one is enough.
$script:Mutex = [Threading.Mutex]::new($false, 'Local\JunOS-Manager')
if (-not $script:Mutex.WaitOne(0)) { exit }
if (-not (Test-Path -LiteralPath (Join-Path $script:Root 'start.ps1'))) {
[System.Windows.MessageBox]::Show(
"start.ps1 isn't next to this program ($($script:Root)). Keep JunOS.exe inside the Jun OS folder.",
'Jun OS', 'OK', 'Error') | Out-Null
exit 1
}
if (-not ('JunProcessOutput' -as [type])) {
Add-Type -TypeDefinition @'
using System.Collections.Concurrent;
using System.Diagnostics;
public sealed class JunProcessOutput
{
public readonly ConcurrentQueue<string> Lines = new ConcurrentQueue<string>();
public void Attach(Process process)
{
process.OutputDataReceived += OnData;
process.ErrorDataReceived += OnData;
}
private void OnData(object sender, DataReceivedEventArgs args)
{
if (args.Data != null)
Lines.Enqueue(args.Data);
}
}
'@
}
[xml]$xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Jun OS"
Width="640" Height="620" MinWidth="540" MinHeight="500"
WindowStartupLocation="CenterScreen"
Background="#111318" Foreground="#F3F4F6"
FontFamily="Segoe UI" FontSize="14">
<Window.Resources>
<Style x:Key="SectionLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#8D95A5" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="2,8,0,2" />
</Style>
<Style TargetType="Button">
<Setter Property="MinWidth" Value="96" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="Margin" Value="0,0,8,8" />
<Setter Property="Background" Value="#2A2E38" />
<Setter Property="Foreground" Value="#F3F4F6" />
<Setter Property="BorderBrush" Value="#464C59" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="ButtonBorder" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#60CDFF" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.45" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="24,20,24,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock FontSize="24" FontWeight="SemiBold">
<Run Foreground="#60CDFF">Ω</Run><Run Text=" Jun OS" />
</TextBlock>
<TextBlock x:Name="FolderText" Foreground="#8D95A5" FontSize="12" Margin="0,4,0,0" TextTrimming="CharacterEllipsis" />
</StackPanel>
<Border Grid.Row="1" Background="#1A1D24" BorderBrush="#343945" BorderThickness="1" CornerRadius="6" Padding="16" Margin="0,16,0,16">
<StackPanel>
<TextBlock x:Name="StateText" FontSize="18" FontWeight="SemiBold" />
<TextBlock x:Name="DetailText" Foreground="#AEB5C2" Margin="0,6,0,0" TextWrapping="Wrap" />
</StackPanel>
</Border>
<StackPanel Grid.Row="2">
<TextBlock Text="Her" Style="{StaticResource SectionLabel}" />
<WrapPanel>
<Button x:Name="StartButton" Content="Start" Background="#0078D4" BorderBrush="#168CE0" />
<Button x:Name="StopButton" Content="Stop" />
<Button x:Name="RestartButton" Content="Restart" />
<Button x:Name="OpenButton" Content="Open Jun OS" />
<Button x:Name="LogsButton" Content="Logs" />
</WrapPanel>
<TextBlock Text="Setup" Style="{StaticResource SectionLabel}" />
<WrapPanel>
<Button x:Name="ConfigureButton" Content="Configure" />
<Button x:Name="UpdateButton" Content="Update" />
<Button x:Name="MtpButton" Content="Tune MTP" />
<Button x:Name="FolderButton" Content="Open folder" />
<Button x:Name="UninstallButton" Content="Uninstall" />
</WrapPanel>
<TextBlock Text="Connections" Style="{StaticResource SectionLabel}" />
<WrapPanel>
<Button x:Name="PhoneButton" Content="Phone" />
<Button x:Name="LinkButton" Content="Notebook link" />
<Button x:Name="BridgeButton" Content="Game bridge" />
<Button x:Name="TrustButton" Content="Certificate" />
</WrapPanel>
</StackPanel>
<TextBox x:Name="OutputBox" Grid.Row="3" IsReadOnly="True" TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas" FontSize="12" Background="#0C0E12" Foreground="#C1C6D0"
BorderBrush="#343945" Padding="8" />
</Grid>
</Window>
'@
$window = [Windows.Markup.XamlReader]::Load([Xml.XmlNodeReader]::new($xaml))
foreach ($name in 'FolderText', 'StateText', 'DetailText', 'StartButton', 'StopButton', 'OpenButton',
'FolderButton', 'LogsButton', 'LinkButton', 'PhoneButton', 'ConfigureButton', 'UpdateButton',
'RestartButton', 'MtpButton', 'BridgeButton', 'TrustButton', 'UninstallButton', 'OutputBox') {
Set-Variable -Name $name -Value $window.FindName($name) -Scope Script
}
$FolderText.Text = $script:Root
function Get-EnvValue([string]$key, [string]$fallback) {
$envPath = Join-Path $script:Root '.env'
if (Test-Path -LiteralPath $envPath) {
$line = Get-Content -LiteralPath $envPath | Where-Object { $_ -match "^$key=" } | Select-Object -Last 1
if ($line) {
$value = $line.Substring($line.IndexOf('=') + 1).Trim()
if ($value -match '^(["''])(.*)\1$') { $value = $Matches[2] }
if ($value) { return $value }
}
}
return $fallback
}
function Get-SiteUrl { return "https://127.0.0.1:$(Get-EnvValue 'JUN_PORT' '8080')" }
function Test-Port([int]$port) {
$tcp = [Net.Sockets.TcpClient]::new()
try {
return $tcp.ConnectAsync('127.0.0.1', $port).Wait(300) -and $tcp.Connected
} catch {
return $false
} finally {
$tcp.Dispose()
}
}
# what start.ps1 launched, straight from runtime\pids.json. cheap
# enough for a 3 s timer, spawning `start.ps1 status` that often
# would not be.
function Get-RunningServices {
$pidFile = Join-Path $script:Root 'runtime\pids.json'
if (-not (Test-Path -LiteralPath $pidFile)) { return @() }
try { $pids = Get-Content -LiteralPath $pidFile -Raw | ConvertFrom-Json } catch { return @() }
$running = @()
foreach ($prop in $pids.PSObject.Properties) {
if (Get-Process -Id $prop.Value -ErrorAction SilentlyContinue) { $running += $prop.Name }
}
return $running
}
$script:busy = $false
$script:then = $null
$script:brushes = [Windows.Media.BrushConverter]::new()
function Set-State([string]$text, [string]$color) {
$StateText.Text = $text
$StateText.Foreground = $script:brushes.ConvertFromString($color)
}
function Update-Status {
$services = @(Get-RunningServices)
# another app on JUN_PORT answers too, so it has to be our caddy
$webUp = 'caddy' -in $services -and (Test-Port ([int](Get-EnvValue 'JUN_PORT' '8080')))
if ($script:busy) {
Set-State 'Working...' '#FFC870'
} elseif ($webUp) {
Set-State 'Running' '#7BD88F'
} elseif ($services.Count -gt 0) {
Set-State 'Partly running' '#FFC870'
} else {
Set-State 'Stopped' '#AEB5C2'
}
$DetailText.Text = if ($services.Count -gt 0) {
'Services: ' + ($services -join ', ') + $(if ($webUp) { "`nOpen at $(Get-SiteUrl)" } else { '' })
} else {
'Nothing running. Press Start to bring her up.'
}
$StartButton.IsEnabled = -not $script:busy
$StopButton.IsEnabled = -not $script:busy -and ($services.Count -gt 0 -or $webUp)
$OpenButton.IsEnabled = $webUp
$UpdateButton.IsEnabled = -not $script:busy
$ConfigureButton.IsEnabled = -not $script:busy
$RestartButton.IsEnabled = -not $script:busy
$MtpButton.IsEnabled = -not $script:busy -and $webUp
$BridgeButton.IsEnabled = -not $script:busy
$TrustButton.IsEnabled = -not $script:busy
$PhoneButton.IsEnabled = -not $script:busy
$UninstallButton.IsEnabled = -not $script:busy
}
function Add-OutputLine([string]$line) {
$plain = [regex]::Replace($line, "$([char]27)\[[0-9;]*[A-Za-z]", '')
$OutputBox.AppendText($plain + [Environment]::NewLine)
$OutputBox.ScrollToEnd()
}
$script:process = $null
$script:output = $null
function Invoke-StartScript([string]$action) {
$info = [Diagnostics.ProcessStartInfo]::new()
$info.FileName = $script:PowerShellExe
$info.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$(Join-Path $script:Root 'start.ps1')`" $action"
$info.WorkingDirectory = $script:Root
$info.UseShellExecute = $false
$info.CreateNoWindow = $true
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$info.StandardOutputEncoding = [Text.Encoding]::UTF8
$info.StandardErrorEncoding = [Text.Encoding]::UTF8
if ($action -eq 'start' -or $action -like 'phone*') { $info.EnvironmentVariables['JUN_LINK_CHECKED'] = '1' }
$script:action = $action
$script:pairLink = $null
$OutputBox.Clear()
$script:process = [Diagnostics.Process]::new()
$script:process.StartInfo = $info
$script:output = [JunProcessOutput]::new()
$script:output.Attach($script:process)
[void]$script:process.Start()
$script:process.BeginOutputReadLine()
$script:process.BeginErrorReadLine()
$script:busy = $true
Update-Status
}
# a script that asks questions (update, uninstall) gets its own
# console window, the person has to be able to answer it.
# ShellExecute because that's what opens a NEW console. without it
# the script version of this window (hidden powershell) hands the
# child its own hidden console and the questions go nowhere.
function Start-ConsoleScript([string]$file, [switch]$FullSetup, [string]$Only = '') {
$env:JUN_DIR = $script:Root
$env:JUN_ONLY = $Only
if ($file -eq 'install.ps1') { $env:JUN_REUSE = if ($FullSetup) { '0' } else { '1' } }
Start-Process -FilePath $script:PowerShellExe -WorkingDirectory $script:Root `
-ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$(Join-Path $script:Root $file)`""
}
function Confirm-Action([string]$text) {
return [System.Windows.MessageBox]::Show($window, $text, 'Jun OS', 'YesNo', 'Question') -eq 'Yes'
}
$timer = [Windows.Threading.DispatcherTimer]::new()
$timer.Interval = [TimeSpan]::FromMilliseconds(200)
$script:ticks = 0
$timer.Add_Tick({
if ($script:output) {
$line = [string]::Empty
while ($script:output.Lines.TryDequeue([ref]$line)) {
if ($line -match '(junos://pair\?\S+)') { $script:pairLink = $Matches[1] }
Add-OutputLine $line
$line = [string]::Empty
}
}
# HasExited, never a bare WaitForExit(). start.ps1's services
# inherit its output pipe and keep it open forever, so waiting
# for the pipe to close freezes this window. same trap as setup.
if ($script:busy -and $script:process -and $script:process.HasExited) {
$script:busy = $false
$code = $script:process.ExitCode
Update-Status
# 3 = start.ps1 refused to start over a -security release
if ($code -eq 3) {
if (Confirm-Action "A release that fixes a security vulnerability is out, so Jun OS won't start on this version.`n`nUpdate now? The installer opens in its own window. Chats and settings stay.") {
Start-ConsoleScript 'install.ps1'
}
} elseif ($code -ne 0) {
$script:then = $null
Add-OutputLine "start.ps1 exited with code $code. The Logs button has the details."
} elseif ($script:action -like 'phone*' -and $script:pairLink) {
Show-PhoneDialog $script:pairLink
} elseif ($script:then) {
$next = $script:then
$script:then = $null
Invoke-StartScript $next
}
}
$script:ticks++
if ($script:ticks % 15 -eq 0) { Update-Status }
})
# start.ps1 runs hidden from here and can't ask for the notebook's
# new lines when its tunnel URL or key changed (every notebook run).
# so the same check as start.ps1 happens here, with a box to paste
# into, and JUN_LINK_CHECKED tells start.ps1 it's been done.
$CloudKeys = @('AI_PROVIDER', 'OLLAMA_URL', 'OLLAMA_API_KEY', 'OLLAMA_MODELS_TO_PULL', 'TITLE_MODEL',
'OMEGA_NUM_CTX', 'VLLM_URL', 'VLLM_API_KEY', 'VLLM_MODEL')
function Get-CloudLink {
switch ((Get-EnvValue 'AI_PROVIDER' 'ollama').ToLower()) {
'vllm' { return @{ Url = (Get-EnvValue 'VLLM_URL' ''); Key = (Get-EnvValue 'VLLM_API_KEY' ''); Probe = '/v1/models' } }
'ollama' {
$url = Get-EnvValue 'OLLAMA_URL' ''
# ollama:11434 is .env.example's docker default, start.ps1 runs a local one for it
if (-not $url -or $url -match '://(127\.0\.0\.1|localhost|\[::1\]|ollama)(:|/|$)') { return $null }
return @{ Url = $url; Key = (Get-EnvValue 'OLLAMA_API_KEY' ''); Probe = '/api/tags' }
}
}
return $null
}
function Test-CloudLink($link) {
if (-not $link.Url) { return $false }
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$headers = @{}
if ($link.Key) { $headers['Authorization'] = "Bearer $($link.Key)" }
try {
Invoke-WebRequest -UseBasicParsing -Uri ($link.Url.TrimEnd('/') + $link.Probe) -Headers $headers -TimeoutSec 8 | Out-Null
return $true
} catch {
return $false
}
}
function Save-CloudLines([string]$text) {
$envPath = Join-Path $script:Root '.env'
$lines = @(Get-Content -LiteralPath $envPath)
foreach ($raw in ($text -split "`r?`n")) {
$line = $raw.Trim()
$eq = $line.IndexOf('=')
if ($eq -lt 1) { continue }
$key = $line.Substring(0, $eq)
if ($key -cnotin $CloudKeys) { continue }
$lines = @($lines | Where-Object { $_ -notmatch "^$key=" }) + $line
}
Set-Content -LiteralPath $envPath -Value $lines
}
# true = go ahead (saved, or start anyway), false = the box got closed
function Show-CloudDialog([string]$reason, [string]$saveLabel, [string]$skipLabel) {
[xml]$dialogXaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Jun OS" Width="560" SizeToContent="Height" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#111318" Foreground="#F3F4F6" FontFamily="Segoe UI" FontSize="14">
<StackPanel Margin="24,20,24,16">
<TextBlock x:Name="ReasonText" TextWrapping="Wrap" />
<TextBlock Text="Paste the lines Step 3 of the notebook printed." Foreground="#AEB5C2" Margin="0,10,0,0" />
<TextBox x:Name="LinesBox" AcceptsReturn="True" TextWrapping="NoWrap" Height="120"
FontFamily="Consolas" FontSize="12" Background="#0C0E12" Foreground="#C1C6D0" BorderBrush="#343945"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Padding="6" Margin="0,8,0,16" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="SaveButton" Padding="14,8" Margin="0,0,8,0" />
<Button x:Name="AnywayButton" Padding="14,8" />
</StackPanel>
</StackPanel>
</Window>
'@
$dialog = [Windows.Markup.XamlReader]::Load([Xml.XmlNodeReader]::new($dialogXaml))
$dialog.Owner = $window
$dialog.FindName('ReasonText').Text = $reason
$dialog.FindName('SaveButton').Content = $saveLabel
$dialog.FindName('AnywayButton').Content = $skipLabel
$box = $dialog.FindName('LinesBox')
$dialog.FindName('SaveButton').Add_Click({
if ($box.Text.Trim()) { Save-CloudLines $box.Text }
$dialog.DialogResult = $true
})
$dialog.FindName('AnywayButton').Add_Click({ $dialog.DialogResult = $skipLabel -ne 'Cancel' })
return $dialog.ShowDialog() -eq $true
}
function Confirm-CloudLink {
$link = Get-CloudLink
if (-not $link -or (Test-CloudLink $link)) { return $true }
$reason = if ($link.Url) {
"Her model server at $($link.Url) isn't answering. A new notebook run means a new URL and key."
} else {
'There is no link to her model server in .env yet.'
}
return Show-CloudDialog $reason 'Save and start' 'Start anyway'
}
function Show-PhoneDialog([string]$link) {
$php = Join-Path $script:Root 'runtime\php\php.exe'
if (-not (Test-Path -LiteralPath $php)) { $php = (Get-Command php -ErrorAction SilentlyContinue).Source }
$rows = @(& $php (Join-Path $script:Root 'tools\qr.php') $link rows | Where-Object { $_ })
if (-not $rows) { Add-OutputLine "Couldn't draw the QR code. The link above works too, paste it into the app."; return }
$quiet = 4
$n = $rows[0].Length + 2 * $quiet
$pixels = [byte[]]::new($n * $n)
for ($i = 0; $i -lt $pixels.Length; $i++) { $pixels[$i] = 255 }
for ($y = 0; $y -lt $rows.Count; $y++) {
for ($x = 0; $x -lt $rows[$y].Length; $x++) {
if ($rows[$y][$x] -eq '1') { $pixels[($y + $quiet) * $n + $x + $quiet] = 0 }
}
}
[xml]$dialogXaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Jun OS" Width="420" SizeToContent="Height" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#111318" Foreground="#F3F4F6" FontFamily="Segoe UI" FontSize="14">
<StackPanel Margin="24,20,24,16">
<Image x:Name="QrImage" Width="320" Height="320" RenderOptions.BitmapScalingMode="NearestNeighbor" />
<TextBlock TextWrapping="Wrap" Margin="0,16,0,0"
Text="Scan it in the Jun OS app (LAN, then Scan) or with the phone camera. Same wifi as this PC. No camera? Get the link below onto the phone and tap Paste link in the app." />
<TextBlock TextWrapping="Wrap" Foreground="#AEB5C2" FontSize="12" Margin="0,8,0,0"
Text="The code holds the token: anyone who scans it can use her model. A new JUN_LAN_TOKEN in .env cuts everyone off." />
<TextBox x:Name="LinkBox" IsReadOnly="True" TextWrapping="Wrap" FontFamily="Consolas" FontSize="11"
Background="#0C0E12" Foreground="#C1C6D0" BorderBrush="#343945" Padding="6" Margin="0,12,0,16" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CopyButton" Content="Copy link" Padding="14,8" Margin="0,0,8,0" />
<Button x:Name="CloseButton" Content="Done" Padding="14,8" />
</StackPanel>
</StackPanel>
</Window>
'@
$dialog = [Windows.Markup.XamlReader]::Load([Xml.XmlNodeReader]::new($dialogXaml))
$dialog.Owner = $window
$dialog.FindName('QrImage').Source = [Windows.Media.Imaging.BitmapSource]::Create(
$n, $n, 96, 96, [Windows.Media.PixelFormats]::Gray8, $null, $pixels, $n)
$dialog.FindName('LinkBox').Text = $link
$copy = $dialog.FindName('CopyButton')
$copy.Add_Click({ [Windows.Clipboard]::SetText($link); $copy.Content = 'Copied' })
$dialog.FindName('CloseButton').Add_Click({ $dialog.Close() })
[void]$dialog.ShowDialog()
}
function Set-ManagerEnv([hashtable]$values) {
$envPath = Join-Path $script:Root '.env'
$lines = @(Get-Content -LiteralPath $envPath)
foreach ($key in $values.Keys) {
$lines = @($lines | Where-Object { $_ -notmatch "^$key=" }) + "$key=$($values[$key])"
}
Set-Content -LiteralPath $envPath -Value $lines
}
function Test-EnvOn([string]$key, [string]$fallback) {
return (Get-EnvValue $key $fallback) -match '^(on|1|true|yes)$'
}
function Show-ConfigureDialog {
[xml]$dialogXaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Jun OS" Width="500" SizeToContent="Height" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#111318" Foreground="#F3F4F6" FontFamily="Segoe UI" FontSize="14">
<StackPanel Margin="24,20,24,16">
<CheckBox x:Name="VoiceBox" Content="Voice (text to speech and the mic)" Foreground="#F3F4F6" />
<CheckBox x:Name="KaraokeBox" Content="Karaoke (adds a few GB, songs split on the CPU here)" Foreground="#F3F4F6" Margin="22,8,0,0" />
<CheckBox x:Name="BridgeBox" Content="Game bridge (her and your game save)" Foreground="#F3F4F6" Margin="0,8,0,0" />
<CheckBox x:Name="UpdateBox" Content="Check for new releases when she starts" Foreground="#F3F4F6" Margin="0,8,0,0" />
<TextBlock TextWrapping="Wrap" Foreground="#AEB5C2" FontSize="12" Margin="0,14,0,16"
Text="Turning voice, karaoke or the bridge on installs what they need, the installer opens in its own window for that. Provider and model live in Full setup." />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="SetupButton" Content="Full setup" Padding="14,8" Margin="0,0,8,0" />
<Button x:Name="SaveButton" Content="Save" Padding="14,8" Margin="0,0,8,0" />
<Button x:Name="CancelButton" Content="Cancel" Padding="14,8" />
</StackPanel>
</StackPanel>
</Window>
'@
$dialog = [Windows.Markup.XamlReader]::Load([Xml.XmlNodeReader]::new($dialogXaml))
$dialog.Owner = $window
$voice = $dialog.FindName('VoiceBox'); $karaoke = $dialog.FindName('KaraokeBox')
$bridge = $dialog.FindName('BridgeBox'); $updates = $dialog.FindName('UpdateBox')
$was = @{ voice = Test-EnvOn 'VOICE' 'on'; karaoke = Test-EnvOn 'KARAOKE' 'off'; bridge = Test-EnvOn 'GAME_BRIDGE' 'off' }
$voice.IsChecked = $was.voice
$karaoke.IsChecked = $was.voice -and $was.karaoke
$karaoke.IsEnabled = $was.voice
$bridge.IsChecked = $was.bridge
$updates.IsChecked = Test-EnvOn 'UPDATE_CHECK' 'off'
$voice.Add_Click({
$karaoke.IsEnabled = $voice.IsChecked -eq $true
if (-not $karaoke.IsEnabled) { $karaoke.IsChecked = $false }
})
$dialog.FindName('CancelButton').Add_Click({ $dialog.Close() })
$dialog.FindName('SetupButton').Add_Click({
$dialog.Close()
Start-ConsoleScript 'install.ps1' -FullSetup
})
$dialog.FindName('SaveButton').Add_Click({
$now = @{ voice = $voice.IsChecked -eq $true; karaoke = $karaoke.IsChecked -eq $true; bridge = $bridge.IsChecked -eq $true }
$onOff = { param($b) if ($b) { 'on' } else { 'off' } }
Set-ManagerEnv @{
VOICE = & $onOff $now.voice; KARAOKE = & $onOff $now.karaoke
GAME_BRIDGE = & $onOff $now.bridge; UPDATE_CHECK = & $onOff ($updates.IsChecked -eq $true)
}
if ($now.karaoke) { Set-ManagerEnv @{ SEP_DEVICE = 'cpu' } }
$dialog.Close()
$only = @()
if (($now.voice -and -not $was.voice) -or ($now.karaoke -and -not $was.karaoke)) { $only += 'voice' }
if ($now.bridge -and -not $was.bridge) { $only += 'bridge' }
if ($only) {
Start-ConsoleScript 'install.ps1' -Only ($only -join ',')
} elseif (@(Get-RunningServices).Count -gt 0) {
Add-OutputLine 'Saved. Restart her to use it.'
}
})
[void]$dialog.ShowDialog()
}
$ConfigureButton.Add_Click({ Show-ConfigureDialog })
$RestartButton.Add_Click({
if (-not (Confirm-CloudLink)) { return }
if (@(Get-RunningServices).Count -gt 0) {
$script:then = 'start'
Invoke-StartScript 'stop'
} else {
Invoke-StartScript 'start'
}
})
$TrustButton.Add_Click({ Invoke-StartScript 'trust' })
$MtpButton.Add_Click({ Start-ConsoleScript 'mtp-autotune.ps1' })
$BridgeButton.Add_Click({
$modIn = (Get-EnvValue 'JUN_GAME_DIR' '') -and (Test-Path -LiteralPath (Join-Path (Get-EnvValue 'JUN_GAME_DIR' '') 'Mods\JunBridge.dll'))
if ($modIn) { Invoke-StartScript 'bridge'; return }
if (Confirm-Action "The game mod isn't installed yet.`n`nInstall it now? A window opens and looks for the game, nothing else changes.") {
Start-ConsoleScript 'install.ps1' -Only 'bridge'
}
})
$StartButton.Add_Click({ if (Confirm-CloudLink) { Invoke-StartScript 'start' } })
$LinkButton.Add_Click({
[void](Show-CloudDialog 'Lend her brain to the Colab or Kaggle notebook in inference only mode. Stop then Start afterwards to use it.' 'Save' 'Cancel')
})
$PhoneButton.Add_Click({
$off = (Get-EnvValue 'LAN_INFERENCE' 'off') -notmatch '^(on|1|true|yes)$' -or
(Get-EnvValue 'JUN_LAN_TOKEN' '').Length -lt 32 -or
(Get-EnvValue 'BIND_ADDR' '127.0.0.1') -in @('127.0.0.1', 'localhost', '::1')
if ($off -and -not (Confirm-Action "Pairing a phone turns on LAN mode. Jun OS then listens on your network (BIND_ADDR=0.0.0.0): other devices on this wifi can reach her login page, and her model with the token in the code.`n`nWrite that to .env and restart her?")) { return }
Invoke-StartScript 'phone -Yes'
})
$StopButton.Add_Click({ Invoke-StartScript 'stop' })
$OpenButton.Add_Click({ Start-Process (Get-SiteUrl) })
$FolderButton.Add_Click({ Start-Process explorer.exe -ArgumentList "`"$($script:Root)`"" })
$LogsButton.Add_Click({
$logs = Join-Path $script:Root 'runtime\logs'
if (-not (Test-Path -LiteralPath $logs)) { $logs = $script:Root }
Start-Process explorer.exe -ArgumentList "`"$logs`""
})
$UpdateButton.Add_Click({
if (-not (Confirm-Action "Update Jun OS now?`n`nThe installer opens in its own window, moves her to the newest version on her release channel and restarts her. Chats and settings stay.")) { return }
Start-ConsoleScript 'install.ps1'
})
$UninstallButton.Add_Click({
if (-not (Confirm-Action "Uninstall Jun OS?`n`nThe uninstaller opens in its own window and asks before deleting anything. This window closes so it can remove the folder.")) { return }
Start-ConsoleScript 'uninstall.ps1'
$window.Close()
})
$window.Add_Closed({
$timer.Stop()
$script:Mutex.ReleaseMutex()
})
Update-Status
$timer.Start()
[void]$window.ShowDialog()