# Installs Girl and LuaXE from https://luaxe.dev # # irm https://luaxe.dev/install.ps1 | iex # # Puts lxe.exe in ~\.lxe\bin and girl.lef + girl.exe in ~\.girl\bin, adds both folders to your # user PATH and registers the girl:// and lxe:// links for the current user. Girl itself is # girl.lef, which lxe.exe runs; girl.exe is a few-KB launcher that runs "lxe.exe girl.lef " # (it finds lxe in ~\.lxe\bin), so `girl` stays the command. lxe keeps its runtime (lua51.dll) # in ~\.lxe\bin and the modules it installs in ~\.lxe\modules\\\; what girl # carries itself is unpacked next to it, in ~\.girl\bin\modules. Run it again to update, # nothing needs administrator rights. Remove everything again with: # # irm https://luaxe.dev/uninstall.ps1 | iex # # Optional environment variables: # LUAXE_URL=http://localhost:4321 download from another server # GIRL_INSTALL_DIR=C:\tools\girl install girl somewhere else # LXE_HOME=C:\tools\lxe lxe's home (bin\ and modules\), as lxe itself reads it # GIRL_NO_PATH=1 leave PATH alone # GIRL_NO_URL_HANDLERS=1 don't register girl:// and lxe:// # GIRL_NO_RESTART=1 after an update, leave running girl processes alone # (by default they restart on the new build and resume) & { $ErrorActionPreference = 'Stop' # the progress bar slows Invoke-WebRequest down a lot in Windows PowerShell $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $base = if ($env:LUAXE_URL) { $env:LUAXE_URL.TrimEnd('/') } else { 'https://luaxe.dev' } $dir = if ($env:GIRL_INSTALL_DIR) { $env:GIRL_INSTALL_DIR } else { Join-Path $HOME '.girl\bin' } $dir = [IO.Path]::GetFullPath($dir).TrimEnd('\') $lxe_home = if ($env:LXE_HOME -and [IO.Path]::IsPathRooted($env:LXE_HOME)) { $env:LXE_HOME } else { Join-Path $HOME '.lxe' } $lxe_dir = [IO.Path]::GetFullPath((Join-Path $lxe_home 'bin')).TrimEnd('\') # Command = a program on PATH (and a URL handler, when it has a Scheme) # SkipInUse = keep the current copy while it is running instead of replacing it: # girl.exe is only the launcher, an older one starts the new girl.lef just as well $files = @( @{ Name = 'lxe.exe'; Dir = $lxe_dir; Url = "$base/get"; Md5 = "$base/md5"; Command = $true; Scheme = 'lxe'; Title = 'LuaXE' }, @{ Name = 'girl.lef'; Dir = $dir; Url = "$base/girl/lef"; Md5 = "$base/girl/lef/md5"; Command = $false }, @{ Name = 'girl.exe'; Dir = $dir; Url = "$base/girl/get"; Md5 = "$base/girl/md5"; Command = $true; Scheme = 'girl'; Title = 'Girl'; SkipInUse = $true } ) # A running exe can't be opened for writing; that is the whole test function in_use([string]$path) { try { [IO.File]::Open($path, 'Open', 'ReadWrite', 'None').Dispose(); $false } catch { $true } } Write-Host "Installing LuaXE to $lxe_dir and Girl to $dir" New-Item -ItemType Directory -Force -Path $dir, $lxe_dir | Out-Null $updated = $false # Every checksum first: a server that is half updated changes nothing. foreach ($file in $files) { $file.Expected = ([string](Invoke-RestMethod -Uri $file.Md5 -UseBasicParsing)).Trim().ToLower() if ($file.Expected -notmatch '^[0-9a-f]{32}$') { throw "$($file.Md5) did not return a checksum" } } foreach ($file in $files) { $target = Join-Path $file.Dir $file.Name # left behind when the previous update replaced a file that was in use Remove-Item "$target.old" -Force -ErrorAction SilentlyContinue if ((Test-Path $target) -and (Get-FileHash $target -Algorithm MD5).Hash.ToLower() -eq $file.Expected) { Write-Host " $($file.Name) is up to date" continue } if ($file.SkipInUse -and (Test-Path $target) -and (in_use $target)) { Write-Host " $($file.Name) is in use, keeping the current one (run this again after closing it to update it)" continue } Write-Host " downloading $($file.Name)" $temp = Join-Path ([IO.Path]::GetTempPath()) ("luaxe-" + [guid]::NewGuid() + ".tmp") Invoke-WebRequest -Uri $file.Url -OutFile $temp -UseBasicParsing $actual = (Get-FileHash $temp -Algorithm MD5).Hash.ToLower() if ($actual -ne $file.Expected) { Remove-Item $temp -Force throw "$($file.Name) failed its checksum (expected $($file.Expected), got $actual)" } if (Test-Path $target) { # a running exe (or a .lef lxe has open) can't be replaced, but it can be renamed out of the way Remove-Item "$target.old" -Force -ErrorAction SilentlyContinue try { Move-Item $target "$target.old" -Force } catch { # the .old from the previous update is still running too, so neither # name is free: keep what is there, the next run gets another chance Remove-Item $temp -Force -ErrorAction SilentlyContinue Write-Host " $($file.Name) is in use and could not be replaced, keeping the current one (run this again after closing it)" continue } } Move-Item $temp $target $updated = $true # still in use when the old version is running, the next update cleans it up Remove-Item "$target.old" -Force -ErrorAction SilentlyContinue } # An older install kept lxe.exe (and lua51.dll) beside girl: the launcher would # keep running that copy, since it looks beside itself first. Retired only now, # once the new lxe is downloaded and verified in its own folder. if (-not ($dir -ieq $lxe_dir)) { foreach ($stale in @('lxe.exe', 'lua51.dll')) { $old = Join-Path $dir $stale if (-not (Test-Path $old)) { continue } Remove-Item "$old.old" -Force -ErrorAction SilentlyContinue try { Move-Item $old "$old.old" -Force } catch { Write-Host " $old is in use, retire it by running this again after closing it" continue } Remove-Item "$old.old" -Force -ErrorAction SilentlyContinue Write-Host " retired $old (lxe lives in $lxe_dir now)" } } $commands = @($files | Where-Object { $_.Command }) if ($env:GIRL_NO_PATH -ne '1') { foreach ($folder in @($dir, $lxe_dir) | Select-Object -Unique) { $environment = Get-Item 'HKCU:\Environment' $raw = [string]$environment.GetValue('Path', '', 'DoNotExpandEnvironmentNames') $parts = @($raw -split ';' | Where-Object { $_ }) $listed = $parts | Where-Object { [Environment]::ExpandEnvironmentVariables($_).TrimEnd('\') -ieq $folder } if (-not $listed) { Set-ItemProperty -Path 'HKCU:\Environment' -Name 'Path' -Value (($parts + $folder) -join ';') -Type ExpandString # setting any user variable broadcasts WM_SETTINGCHANGE, so new terminals see the new PATH [Environment]::SetEnvironmentVariable('GIRL_INSTALLER', '1', 'User') [Environment]::SetEnvironmentVariable('GIRL_INSTALLER', $null, 'User') Write-Host " added $folder to your PATH" } if (-not (@($env:Path -split ';') | Where-Object { $_.TrimEnd('\') -ieq $folder })) { $env:Path = "$folder;$env:Path" } } foreach ($file in $commands) { $found = Get-Command $file.Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($found -and -not ($found.Source -ieq (Join-Path $file.Dir $file.Name))) { Write-Warning "$($found.Source) comes first in PATH, '$($file.Name -replace '\.exe$')' will run that one" } } } if ($env:GIRL_NO_URL_HANDLERS -ne '1') { foreach ($file in @($commands | Where-Object { $_.Scheme })) { $root = "HKCU:\Software\Classes\$($file.Scheme)" $target = Join-Path $file.Dir $file.Name foreach ($key in @($root, "$root\DefaultIcon", "$root\shell\open\command")) { if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } } Set-Item -Path $root -Value "URL:$($file.Title) Protocol" Set-ItemProperty -Path $root -Name 'URL Protocol' -Value '' # girl.exe is the launcher, which has no icon of its own: show lxe's $icon = Join-Path $lxe_dir 'lxe.exe' Set-Item -Path "$root\DefaultIcon" -Value "`"$icon`",0" Set-Item -Path "$root\shell\open\command" -Value "`"$target`" `"%1`"" } Write-Host " registered girl:// and lxe:// links" } # First start: lxe unpacks the module girl carries (win32) and girl installs the published # ones it needs (memory, thread, network, lfs, socket, crypto and the voice modules webtransport # and webrtc) into lxe's store, $lxe_home\modules. Done here so a problem shows up now, # not on the first `girl` in some project. Write-Host " preparing modules" # Windows PowerShell turns a native command's redirected stderr into error # records, which 'Stop' would make fatal: a first start that only warns (or # fails) must end in the warning below, not abort an install that is done. $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' # captured right after the call: a girl.exe that did not even start leaves the # PREVIOUS native command's code here, which must not pass for success $LASTEXITCODE = $null try { $version = & (Join-Path $dir 'girl.exe') version 2>&1 | ForEach-Object { "$_" } | Out-String $warm_exit = $LASTEXITCODE } catch { $version = "$_" $warm_exit = 1 } finally { $ErrorActionPreference = $saved } if ($warm_exit -ne 0) { Write-Warning "girl did not start yet ($($version.Trim())); run 'girl version' once you are online" } else { Write-Host " $($version.Trim())" } # An update while girl is running: every girl process (sessions, girl app and its # fleet) restarts on the new build at its next step boundary and resumes its # session. Only when something was replaced and girl runs; GIRL_NO_RESTART=1 opts out. $girl_state = Join-Path $HOME '.girl' $running = @(Get-ChildItem (Join-Path $girl_state 'apps'), (Join-Path $girl_state 'instances') ` -Filter '*.json' -ErrorAction SilentlyContinue).Count -gt 0 if ($warm_exit -eq 0 -and $updated -and $running -and $env:GIRL_NO_RESTART -ne '1') { Write-Host " restarting running girl processes, each after its current step (this can take a minute or two; set GIRL_NO_RESTART=1 to skip)" $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & (Join-Path $dir 'girl.exe') update --restart-only 2>&1 | ForEach-Object { Write-Host " $_" } } catch { Write-Warning "could not restart running girl processes ($_); restart them by hand" } finally { $ErrorActionPreference = $saved } } Write-Host '' Write-Host 'Girl and LuaXE are installed.' -ForegroundColor Green Write-Host ' girl chat start the coding agent in the current folder' Write-Host ' lxe init create a new LuaXE project' Write-Host "Docs: $base/girl/docs and $base/docs" Write-Host '' Write-Host "Girl is licensed under its EULA ($base/girl/license): free for non-commercial use," Write-Host 'any for-profit use needs a commercial license.' }