#Requires -Version 5.1 <# OpenAI4S launcher for Windows. ASCII ONLY, and not as a style preference. `OpenAI4S.cmd` invokes `powershell.exe` -- Windows PowerShell 5.1, which is what a user actually double-clicks -- and 5.1 reads a .ps1 without a BOM as ANSI, not UTF-8. A UTF-8 em dash decodes under cp1252 to three characters ending in 0x94, which is U+201D, which PowerShell accepts as a closing double quote: a dash inside a string literal silently ends the string and the parse collapses far below it. This file had exactly that, and pwsh 7 parsed it happily while 5.1 failed at `if (Test-Serving) {`, 200 lines away from the real cause. `verify_windows_zip.py` fails the build on any non-ASCII byte here. This package does NOT run OpenAI4S on native Windows, and that is deliberate rather than a limitation of the packaging. The kernel spawns POSIX subprocesses, the R channel rides file descriptors 3 and 4 through a shell redirection, and the OS sandbox has no Windows backend -- so openai4s/platform_support.py refuses to start a kernel on win32 instead of warning and proceeding. A Windows build that started anyway would leave a scientist to discover the problem from a half-working analysis, which is precisely the failure this product cannot afford. So the Windows deliverable is the documented supported route, made double-clickable: the package carries the Linux bundle, installs it into your WSL2 distribution on first launch, starts the daemon there, and opens the Windows browser at the forwarded localhost port. WSL2 reports as Linux, which is a supported platform, so nothing is being worked around -- the app runs on the platform it says it runs on. Usage: OpenAI4S.cmd start the daemon and open the web UI OpenAI4S.cmd status ask the daemon whether it is up OpenAI4S.cmd setup any openai4s CLI command, run inside WSL2 Environment: OPENAI4S_WSL_DISTRO use this distribution instead of the default OPENAI4S_WSL_PROXY proxy reachable from WSL (for example http://127.0.0.1:7897 with mirrored networking) OPENAI4S_WSL_PYPI_INDEX Python mirror used by later package installs OPENAI4S_WSL_CONDA_MIRROR Conda mirror root used by environment setup OPENAI4S_WSL_FAKE_IP_DNS auto (default), on, or off for Clash-style DNS OPENAI4S_WSL_DATA_DIR optional absolute Linux data path (default ~/.openai4s) OPENAI4S_ARKCLI_PATH optional Windows executable or absolute WSL CLI path OPENAI4S_HOST default 127.0.0.1 OPENAI4S_PORT default 8760 OPENAI4S_NO_OPEN set to 1 to print readiness without opening a browser #> [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]] $Arguments ) $ErrorActionPreference = 'Stop' # wsl.exe emits UTF-16LE by default, which turns every parsed line into # null-separated mush. WSL 0.64+ honours this; older builds are handled by the # null-stripping in Get-WslDistros. $env:WSL_UTF8 = '1' $Here = $PSScriptRoot $BindHost = if ($env:OPENAI4S_HOST) { $env:OPENAI4S_HOST } else { '127.0.0.1' } $ClientHost = switch ($BindHost) { '0.0.0.0' { '127.0.0.1'; break } '::' { '::1'; break } default { $BindHost } } # Compatibility name retained for the packaged verifier. It is always the # client-facing host; binding and WSL environment propagation use $BindHost. $AppHost = $ClientHost $AppPort = if ($env:OPENAI4S_PORT) { $env:OPENAI4S_PORT } else { '8760' } $WslProxy = $env:OPENAI4S_WSL_PROXY $WslDataDir = $env:OPENAI4S_WSL_DATA_DIR # Resolved after distribution selection, and only for non-management commands. $WslArkCli = '' $FakeIpDnsMode = if ($env:OPENAI4S_WSL_FAKE_IP_DNS) { $env:OPENAI4S_WSL_FAKE_IP_DNS.Trim().ToLowerInvariant() } else { 'auto' } $WslLogPath = if ($WslDataDir) { "$WslDataDir/logs/app.out" } else { # A tilde, because the command this ends up in runs through WSL's default # shell, which expands it. Neither cmd.exe nor PowerShell touches a bare # `~` on the way there, where both would substitute `$HOME`. '~/.openai4s/logs/app.out' } $PypiIndex = if ($env:OPENAI4S_WSL_PYPI_INDEX) { $env:OPENAI4S_WSL_PYPI_INDEX } else { 'https://pypi.tuna.tsinghua.edu.cn/simple' } $CondaMirror = if ($env:OPENAI4S_WSL_CONDA_MIRROR) { $env:OPENAI4S_WSL_CONDA_MIRROR } else { 'https://mirrors.tuna.tsinghua.edu.cn/anaconda' } # Windows cannot express an empty environment variable (set VAR= deletes it), # so `off` is the explicit way to say "no mirror: use the official indexes". $PypiIndexOff = $PypiIndex -eq 'off' $CondaMirrorOff = $CondaMirror -eq 'off' if ($PypiIndex -eq 'off') { $PypiIndex = '' } if ($CondaMirror -eq 'off') { $CondaMirror = '' } function Get-AppBaseUrl([string] $HostValue, [string] $PortValue) { $address = $null $urlHost = $HostValue if ([Net.IPAddress]::TryParse($HostValue, [ref] $address) -and $address.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetworkV6) { $urlHost = "[$HostValue]" } return "http://${urlHost}:${PortValue}/" } $Url = Get-AppBaseUrl $ClientHost $AppPort function Write-Section([string] $Text) { Write-Host '' Write-Host $Text -ForegroundColor Cyan } function Open-AppUrl([string] $appUrl) { if ($env:OPENAI4S_NO_OPEN) { Write-Host ' browser open suppressed by OPENAI4S_NO_OPEN.' -ForegroundColor DarkGray # Print it. The caller already paid a wsl.exe round trip for this exact # string, and the readiness line above it carries no sign-in token, so # sending the user back to OpenAI4S.cmd url would be advice instead of # the answer we are holding. Write-Host " $appUrl" -ForegroundColor DarkGray return } try { Start-Process $appUrl } catch { Write-Host ' the default browser could not be opened. Open this URL:' -ForegroundColor Yellow Write-Host " $appUrl" -ForegroundColor Yellow } } function Get-WslLogCommand([string] $Distro) { # No `--exec` and no inner `sh -lc`: without `--exec`, wsl.exe runs the line # through the distribution's default shell, so `~` still expands and the # command needs no single quotes. That matters because `OpenAI4S.cmd` is a # cmd.exe wrapper -- cmd does not treat `'` as quoting, so the previous # form pasted back as `sh -lc 'tail` and died on an unterminated string, in # all four of the fatal paths that print it. Double quotes around the # distribution name are honoured by cmd and PowerShell alike, and # `Assert-WslDataDir` refuses whitespace so the path never needs any. return "wsl -d `"$Distro`" -- tail -40 $WslLogPath" } function Set-UrlHost([string] $Url, [string] $NewHost) { # `openai4s url` can only know the host the daemon was told to BIND. When # that is the wildcard, the CLI renders it as `localhost` -- the one address # Windows cannot reach with localhostForwarding=false, which is exactly the # case the NAT fallback exists for. The sign-in token lives in the query, so # re-authority the URL rather than discarding it. try { $builder = New-Object System.UriBuilder $Url if ($builder.Host -ne $NewHost) { $builder.Host = $NewHost } return $builder.Uri.AbsoluteUri } catch { return $Url } } function Stop-WithGuidance([string] $Problem, [string[]] $Steps) { Write-Host '' Write-Host "OpenAI4S cannot start: $Problem" -ForegroundColor Red if ($Steps) { Write-Host '' foreach ($step in $Steps) { Write-Host " $step" } } # Double-clicked from Explorer, the console window closes the instant this # returns and the user sees the guidance for about a frame. The pause is # skipped when something is driving the launcher -- CI, or a script -- where # there is nobody to press a key and blocking would look like a hang. if (-not $env:OPENAI4S_NONINTERACTIVE) { Write-Host '' Write-Host 'Press Enter to close.' -ForegroundColor DarkGray try { [void](Read-Host) } catch { } } exit 1 } function Enter-Utf8NativeOutput { # WSL_UTF8 selects the bytes WSL writes; PowerShell 5.1 separately # decodes native stdout with Console.OutputEncoding (often OEM). Returns # the encoding to restore, or $null when nothing changed: a host with no # console (the ISE, a CREATE_NO_WINDOW parent) refuses SetConsoleOutputCP, # and failing to change a decoder must not abort the launcher. try { $previous = [Console]::OutputEncoding [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false) return $previous } catch { return $null } } function Exit-Utf8NativeOutput($Previous) { if ($null -eq $Previous) { return } try { [Console]::OutputEncoding = $Previous } catch { } } function Invoke-WslCaptureNative([string[]] $WslArgs) { # Windows PowerShell 5.1 converts native stderr into ErrorRecord objects. # With this script's fail-fast ErrorActionPreference, a harmless WSL # diagnostic (notably the NAT localhost-proxy warning) otherwise aborts a # command whose native exit code is zero. Native tools are authoritative # through LASTEXITCODE; capture both streams while that one call is allowed # to continue, then restore the script-wide fail-fast policy. $previousPreference = $ErrorActionPreference $previousEncoding = $null $output = @() $code = 1 try { $ErrorActionPreference = 'Continue' $previousEncoding = Enter-Utf8NativeOutput $output = @(& wsl.exe @WslArgs 2>&1) $code = $LASTEXITCODE } finally { Exit-Utf8NativeOutput $previousEncoding $ErrorActionPreference = $previousPreference } return [pscustomobject]@{ ExitCode = $code Output = $output } } function Get-PackageFacts { $versionFile = Join-Path $Here 'VERSION' if (-not (Test-Path $versionFile)) { Stop-WithGuidance 'this package is incomplete (no VERSION file).' @( 'Re-download the release archive and unzip it again.' ) } $version = (Get-Content $versionFile -Raw).Trim() $payload = Get-ChildItem -Path (Join-Path $Here 'payload') -Filter '*.tar.gz' -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $payload) { Stop-WithGuidance 'this package carries no Linux payload.' @( 'Re-download the release archive and unzip the whole thing --', 'the payload folder is not optional.' ) } $digestFile = "$($payload.FullName).sha256" if (-not (Test-Path $digestFile)) { Stop-WithGuidance 'the payload has no checksum file beside it.' @( 'Re-download the release archive and unzip it again.' ) } # ` ` -- the same one-line format both build scripts publish. $digest = ((Get-Content $digestFile -Raw).Trim() -split '\s+')[0] [pscustomobject]@{ Version = $version PayloadPath = $payload.FullName PayloadName = $payload.Name Digest = $digest # The directory the tarball unpacks into, which is its own name minus # the two archive suffixes. BundleDir = $payload.Name -replace '\.tar\.gz$', '' } } function Get-WslDistros { if (-not (Get-Command 'wsl.exe' -ErrorAction SilentlyContinue)) { Stop-WithGuidance 'WSL is not installed on this machine.' @( 'OpenAI4S runs on Linux; on Windows it runs inside WSL2.', '', 'Open PowerShell as Administrator and run:', ' wsl --install -d Ubuntu-24.04', '', 'Reboot when it asks, finish the Ubuntu username/password prompt,', 'then run OpenAI4S.cmd again.' ) } $listing = Invoke-WslCaptureNative -WslArgs @('--list', '--verbose') $raw = $listing.Output if ($listing.ExitCode -ne 0) { Stop-WithGuidance 'WSL is present but has no installed distribution.' @( 'Open PowerShell as Administrator and run:', ' wsl --install -d Ubuntu-24.04', '', 'Finish the username/password prompt, then run OpenAI4S.cmd again.' ) } $distros = @() foreach ($line in @($raw)) { # Strip the UTF-16 nulls an older wsl.exe emits despite WSL_UTF8. $text = ([string]$line) -replace "`0", '' $text = $text.Trim() if (-not $text) { continue } $isDefault = $text.StartsWith('*') $text = $text.TrimStart('*').Trim() # Split on runs of two or more spaces, because that is what separates # the padded columns. Splitting on any whitespace cannot tell a name # containing a space ("My Distro") from a localized multi-word STATE # ("Wird ausgeftihrt", "En cours d'execution") -- and this listing is # localized, as the header-row comment below already acknowledges. # Single-space separation (a name long enough to eat the padding) falls # back to the first field only, which is what this read before names # with spaces were supported. $paddedColumns = $true $fields = $text -split '\s{2,}' if ($fields.Count -lt 3) { $paddedColumns = $false $fields = $text -split '\s+' } if ($fields.Count -lt 3) { continue } # The header row, in whatever language Windows is running in, is the one # whose last field is not a number. $wslVersion = 0 if (-not [int]::TryParse($fields[-1], [ref] $wslVersion)) { continue } $state = $fields[-2] $name = if ($paddedColumns) { @($fields[0..($fields.Count - 3)]) -join ' ' } else { $fields[0] } $distros += [pscustomobject]@{ Name = $name State = $state Version = $wslVersion IsDefault = $isDefault } } return $distros } function Select-Distro { $distros = Get-WslDistros # These distributions belong to other desktop applications, not the user. $distros = @($distros | Where-Object { $_.Name -notmatch '^(docker-desktop|rancher-desktop|podman-machine)(-|$)' }) if (-not $distros -or $distros.Count -eq 0) { Stop-WithGuidance 'no user-owned WSL distribution is available.' @( 'Open PowerShell as Administrator and run:', ' wsl --install -d Ubuntu-24.04' ) } if ($env:OPENAI4S_WSL_DISTRO) { $named = $distros | Where-Object { $_.Name -eq $env:OPENAI4S_WSL_DISTRO } | Select-Object -First 1 if (-not $named) { Stop-WithGuidance "OPENAI4S_WSL_DISTRO names '$($env:OPENAI4S_WSL_DISTRO)', which is not installed." @( 'Installed distributions:', ($distros | ForEach-Object { " $($_.Name) (WSL $($_.Version))" }) ) } if ($named.Version -ne 2) { Stop-WithGuidance "'$($named.Name)' is a WSL 1 distribution." @( 'WSL 1 emulates Linux syscalls and has no user namespaces, so the', 'kernel sandbox cannot start and cells would run unisolated.', '', 'Convert it:', " wsl --set-version `"$($named.Name)`" 2" ) } return $named.Name } $two = @($distros | Where-Object { $_.Version -eq 2 }) if ($two.Count -eq 0) { $names = ($distros | ForEach-Object { $_.Name }) -join ', ' Stop-WithGuidance "every installed distribution ($names) is WSL 1." @( 'WSL 1 emulates Linux syscalls and has no user namespaces, so the', 'kernel sandbox cannot start and cells would run unisolated.', '', 'Convert one:', " wsl --set-version `"$($distros[0].Name)`" 2", '', 'Or set OPENAI4S_WSL_DISTRO to a WSL 2 distribution.' ) } # A distribution that already holds OpenAI4S data pins the choice. Without # this, installing Ubuntu 24.04 for any reason would silently move the app # to a fresh distribution and every existing session would look deleted. $withData = @($two | Where-Object { Test-DistroHasInstall $_.Name }) $pool = if ($withData.Count -gt 0) { $withData } else { $two } # Ubuntu 24.04 carries bubblewrap >= 0.8.0. Prefer it when an older distro # is still the Windows default; the explicit environment override above # remains authoritative for people using another compatible distribution. $selected = $pool | Where-Object { $_.Name -eq 'Ubuntu-24.04' } | Select-Object -First 1 if (-not $selected) { $selected = $pool | Where-Object { $_.IsDefault } | Select-Object -First 1 } if (-not $selected) { $selected = $pool[0] } if ($withData.Count -gt 0) { Write-Host " using WSL distribution $($selected.Name) (existing OpenAI4S data found there)." -ForegroundColor DarkGray } return $selected.Name } function Test-DistroHasInstall([string] $Name) { # `wsl --exec` starts no shell, so $HOME needs sh to expand it; the # data-dir override is a PowerShell-side literal that Assert-WslDataDir has # already stripped of quoting and expansion characters. $probeScript = if ($WslDataDir) { "test -d `"$WslDataDir/app`"" } else { 'test -d "$HOME/.openai4s/app"' } $probe = Invoke-WslCaptureNative -WslArgs @( '-d', $Name, '--exec', 'sh', '-c', $probeScript ) return $probe.ExitCode -eq 0 } function Test-LocalhostForwardingDisabled { # `localhostForwarding=false` is an explicit request, not a transient # connection failure. In NAT mode it means a daemon bound to WSL loopback # can never be reached by the Windows browser. Mirrored networking has one # shared loopback and therefore does not need the NAT-address fallback. $configPath = Join-Path $env:USERPROFILE '.wslconfig' if (-not (Test-Path -LiteralPath $configPath)) { return $false } $section = '' $disabled = $false $mirrored = $false foreach ($rawLine in Get-Content -LiteralPath $configPath) { $line = ([string] $rawLine).Trim() if ($line -match '^\[([^]]+)\]$') { $section = $Matches[1].Trim().ToLowerInvariant() continue } if ($section -ne 'wsl2' -or -not $line -or $line.StartsWith('#')) { continue } if ($line -match '^localhostForwarding\s*=\s*([^#;]+)') { $value = $Matches[1].Trim().ToLowerInvariant() $disabled = $value -in @('false', '0', 'no', 'off') } if ($line -match '^networkingMode\s*=\s*([^#;]+)') { $mirrored = $Matches[1].Trim().ToLowerInvariant() -eq 'mirrored' } } return $disabled -and -not $mirrored } function Get-WslIpv4([string] $Distro) { # eth0 is the WSL NAT interface Windows can route back to. Prefer it over # docker0, VPNs, and other bridges regardless of hostname -I ordering. $primary = Invoke-WslCaptureNative -WslArgs @( '-d', $Distro, '--exec', 'ip', '-4', '-o', 'addr', 'show', 'dev', 'eth0', 'scope', 'global' ) if ($primary.ExitCode -eq 0) { foreach ($item in @($primary.Output)) { $clean = (([string] $item) -replace "`0", '').Trim() $tokens = @($clean -split '\s+') for ($index = 0; $index -lt ($tokens.Count - 1); $index++) { if ($tokens[$index] -ne 'inet') { continue } $candidate = @($tokens[$index + 1] -split '/', 2)[0] $address = $null if ([Net.IPAddress]::TryParse($candidate, [ref] $address) -and $address.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and -not [Net.IPAddress]::IsLoopback($address)) { return $address.IPAddressToString } } } } # Non-standard WSL distributions may rename the primary interface. Ask # Linux routing for their default-path source before the broad fallback. $route = Invoke-WslCaptureNative -WslArgs @( '-d', $Distro, '--exec', 'ip', '-4', 'route', 'get', '192.0.2.1' ) if ($route.ExitCode -eq 0) { foreach ($item in @($route.Output)) { $clean = (([string] $item) -replace "`0", '').Trim() $tokens = @($clean -split '\s+') for ($index = 0; $index -lt ($tokens.Count - 1); $index++) { if ($tokens[$index] -ne 'src') { continue } $address = $null if ([Net.IPAddress]::TryParse($tokens[$index + 1], [ref] $address) -and $address.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and -not [Net.IPAddress]::IsLoopback($address)) { return $address.IPAddressToString } } } } # Old/minimal distributions may not carry iproute2. Keep a compatibility # fallback, but only after the route-selected address was unavailable. $result = Invoke-WslCaptureNative -WslArgs @( '-d', $Distro, '--exec', 'hostname', '-I' ) if ($result.ExitCode -ne 0) { return $null } foreach ($item in @($result.Output)) { $clean = (([string] $item) -replace "`0", '').Trim() foreach ($token in $clean -split '\s+') { $address = $null if ([Net.IPAddress]::TryParse($token, [ref] $address) -and $address.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and -not [Net.IPAddress]::IsLoopback($address)) { return $address.IPAddressToString } } } return $null } function ConvertTo-WslPath([string] $Distro, [string] $WindowsPath, [switch] $Optional) { # -Optional returns '' instead of refusing: the guidance below is about # this package's own folder, not about an optional tool found elsewhere. $result = Invoke-WslCaptureNative -WslArgs @( '-d', $Distro, '--exec', 'wslpath', '-a', $WindowsPath ) $translated = $result.Output if ($result.ExitCode -ne 0) { if ($Optional) { return '' } Stop-WithGuidance "WSL could not reach this folder: $WindowsPath" @( 'Unzip the package onto a local drive (for example C:\OpenAI4S).', 'A network share or a OneDrive placeholder folder is not always', 'visible from inside WSL.' ) } # The outer @() is load-bearing: PowerShell unwraps a one-item pipeline to # a scalar string, and indexing that scalar returns its first character. $paths = @( @($translated) | ForEach-Object { (([string] $_) -replace "`0", '').Trim() } | Where-Object { $_.StartsWith('/') } ) if (-not $paths) { if ($Optional) { return '' } Stop-WithGuidance "WSL returned no Linux path for: $WindowsPath" @( 'Unzip the package onto a local drive and try again.' ) } $selectedPath = $paths[0] return [string]$selectedPath } function Assert-HttpUrl([string] $Name, [string] $Value) { if ([string]::IsNullOrWhiteSpace($Value)) { return } $parsed = $null if (-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref] $parsed) -or $parsed.Scheme -notin @('http', 'https') -or -not [string]::IsNullOrEmpty($parsed.UserInfo)) { Stop-WithGuidance "$Name is not an absolute HTTP(S) URL." @( 'Use a credential-free value such as http://127.0.0.1:7897.' ) } } function Assert-WslDataDir([string] $Value) { if ([string]::IsNullOrWhiteSpace($Value)) { return } $parts = $Value -split '/' # Quoting and expansion characters are refused, not escaped: the value is # interpolated into sh command strings (Test-DistroHasInstall) where a # quote or `$` would change what the shell runs. # Whitespace is refused for the same reason as the quoting characters, not # a stricter one: `Get-WslLogCommand` prints this path as a command the user # is meant to paste into cmd.exe, where a space would split it in two. if (-not $Value.StartsWith('/') -or $Value -eq '/' -or $parts -contains '..' -or $Value -match '\s' -or $Value.IndexOfAny([char[]] @('"', "'", '`', '$', '\')) -ge 0) { Stop-WithGuidance 'OPENAI4S_WSL_DATA_DIR must be a specific absolute Linux path.' @( 'Example: /home/me/.openai4s (no spaces, quotes, backslashes, or $)' ) } } function Get-WslArkCliPath([string] $Distro) { # Ark CLI is optional. Only an explicit OPENAI4S_ARKCLI_PATH may refuse a # launch; an arkcli.exe that merely happens to be on PATH is skipped when # WSL cannot reach it (a UNC share it does not mount, for example). $configured = $env:OPENAI4S_ARKCLI_PATH if ($configured -and $configured.StartsWith('/')) { return $configured } $source = '' if ($configured -and $configured.IndexOfAny([char[]] @('\', '/', ':')) -ge 0) { # A path is checked literally: Get-Command reads `[` and `]` as # wildcards, and both are legal in a Windows folder name. try { if (Test-Path -LiteralPath $configured -PathType Leaf) { $source = $configured } } catch { } } elseif (-not $configured -or $configured.IndexOfAny([char[]] @('*', '?', '[', ']')) -lt 0) { # The first arkcli.exe in PATH order, not the first Application: an # npm arkcli.cmd earlier on PATH would otherwise hide it. A configured # name with wildcard characters is refused below instead: Get-Command # would expand `*` into whatever .exe comes first, such as cmd.exe. $name = if ($configured) { $configured } else { 'arkcli.exe' } $command = Get-Command $name -CommandType Application -All -ErrorAction SilentlyContinue | Where-Object { $_.Source.EndsWith('.exe', [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1 if ($command) { $source = $command.Source } } if ($source -and $source.EndsWith('.exe', [StringComparison]::OrdinalIgnoreCase)) { $translated = ConvertTo-WslPath $Distro $source -Optional if ($translated) { return $translated } if (-not $configured) { Write-Host " note: $source is not reachable from WSL; not using it for Volcengine login." -ForegroundColor DarkGray return '' } Stop-WithGuidance "WSL cannot reach OPENAI4S_ARKCLI_PATH: $source" @( 'Copy arkcli.exe to a local drive, or install the CLI inside WSL', 'and set OPENAI4S_ARKCLI_PATH to its absolute Linux path.' ) } if ($configured) { Stop-WithGuidance "OPENAI4S_ARKCLI_PATH does not name an installed executable: $configured" @( 'Use the full Windows path to arkcli.exe, or an absolute Linux', 'path to a CLI installed inside the selected WSL distribution.' ) } return '' } function Get-WslBootstrapArgs([string] $Distro, [string] $BootstrapLinux, [string[]] $BootstrapArgs, [string] $User = '') { $wslArgs = @('-d', $Distro) if ($User) { $wslArgs += @('-u', $User) } $wslArgs += @('--exec', 'env') $wslArgs += "OPENAI4S_HOST=$BindHost" $wslArgs += "OPENAI4S_PORT=$AppPort" if ($WslDataDir) { $wslArgs += "OPENAI4S_DATA_DIR=$WslDataDir" } if ($WslArkCli) { # Only an explicit choice overrides a CLI installed inside WSL; one the # launcher merely found on the Windows PATH is the daemon's fallback. $arkVariable = if ($env:OPENAI4S_ARKCLI_PATH) { 'OPENAI4S_ARKCLI_PATH' } else { 'OPENAI4S_ARKCLI_FALLBACK_PATH' } $wslArgs += "$arkVariable=$WslArkCli" } $wslArgs += "OPENAI4S_FAKE_IP_DNS_MODE=$FakeIpDnsMode" if ($PypiIndexOff) { $wslArgs += 'OPENAI4S_PYPI_INDEX_URL=off' $wslArgs += 'PIP_INDEX_URL=' $wslArgs += 'UV_DEFAULT_INDEX=' } elseif ($PypiIndex) { $wslArgs += "OPENAI4S_PYPI_INDEX_URL=$PypiIndex" $wslArgs += "PIP_INDEX_URL=$PypiIndex" $wslArgs += "UV_DEFAULT_INDEX=$PypiIndex" } if ($CondaMirrorOff) { $wslArgs += 'OPENAI4S_CONDA_MIRROR=off' } elseif ($CondaMirror) { $wslArgs += "OPENAI4S_CONDA_MIRROR=$CondaMirror" } $proxyBypass = "127.0.0.1,localhost,$AppHost" if ($BindHost -ne $AppHost) { $proxyBypass += ",$BindHost" } $wslArgs += "NO_PROXY=$proxyBypass" $wslArgs += "no_proxy=$proxyBypass" if ($WslProxy) { $wslArgs += "HTTP_PROXY=$WslProxy" $wslArgs += "HTTPS_PROXY=$WslProxy" $wslArgs += "http_proxy=$WslProxy" $wslArgs += "https_proxy=$WslProxy" } $wslArgs += @('sh', $BootstrapLinux) $wslArgs += $BootstrapArgs return $wslArgs } function Invoke-Bootstrap([string] $Distro, [string] $BootstrapLinux, [string[]] $BootstrapArgs, [string] $User = '') { # --exec: run without an intervening login shell, so argv reaches the script # as-is. Plain `wsl ` hands the line to the default shell, which # re-splits it and breaks on the spaces an unzipped Downloads path is full # of. `sh