#Requires -Version 5.1
# Pure generator: collected values -> Windows Setup answer-file XML string.
# No WinForms dependency (unit-testable). Mirrors the legacy autounattend.xml but with
# ONE real local-admin account (no sm-bootstrap) and an embedded preconfig.json that the
# oobeSystem FirstLogonCommands write (in short base64 chunks) to
# C:\ProgramData\SilverMetal\preconfig.json. The base64 is carried in chunked echo
# commands rather than a single specialize RunSynchronousCommand/Path, because that Path
# is capped at ~259 chars and a full base64 blob overflows it -> "answer file is invalid".
function New-SmAnswerFile {
param(
[string]$DisplayName, [string]$Username, [string]$Password,
[string]$ComputerName,
[string]$InputLocale = '0809:00000809', [string]$SystemLocale = 'en-GB',
[string]$UiLanguage = 'en-US', [string]$UserLocale = 'en-GB',
[string]$Flavour, [bool]$BitLockerEnable = $false, [string]$BitLockerPin = ''
)
$pre = [ordered]@{
schemaVersion = 1
flavour = $Flavour
bitlocker = [ordered]@{ enable = [bool]$BitLockerEnable; pin = $BitLockerPin }
apps = [ordered]@{ useFlavourDefaults = $true }
}
$preJson = ($pre | ConvertTo-Json -Depth 6 -Compress)
$preB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($preJson))
function Esc([string]$s) { [Security.SecurityElement]::Escape($s) }
# Escape ONLY the characters XML element content requires (& < >). Unlike
# SecurityElement::Escape this leaves single/double quotes literal, so the
# embedded command keeps a working FromBase64String('...') literal.
function EscContent([string]$s) { $s.Replace('&','&').Replace('<','<').Replace('>','>') }
$dn = Esc $DisplayName; $un = Esc $Username; $pw = Esc $Password; $cn = Esc $ComputerName
# Build the oobeSystem FirstLogonCommands. The preconfig base64 is split into short
# (<=150 char) chunks, each appended to a temp file by its own `echo` command, then
# the file is whitespace-stripped + base64-decoded into preconfig.json. This keeps
# every single command line well under the unattend length limits.
$preDir = 'C:\ProgramData\SilverMetal'
$preB64File = "$preDir\pre.b64"
$preFile = "$preDir\preconfig.json"
# Split base64 into chunks of at most 150 chars (base64 alphabet has no XML/cmd
# metachars, so chunks are safe in `echo` and in XML once `>` is escaped).
$chunkSize = 150
$chunks = for ($i = 0; $i -lt $preB64.Length; $i += $chunkSize) {
$preB64.Substring($i, [Math]::Min($chunkSize, $preB64.Length - $i))
}
$cmds = New-Object System.Collections.Generic.List[string]
# 1. Create the target dir.
$cmds.Add("cmd /c md ""$preDir"" 2>nul")
# 2..N. Append each base64 chunk to the temp file.
foreach ($c in $chunks) {
$cmds.Add("cmd /c >>""$preB64File"" echo $c")
}
# N+1. Strip whitespace (chunks are newline-separated in the file) and decode.
$cmds.Add("powershell -nop -c ""[IO.File]::WriteAllBytes('$preFile',[Convert]::FromBase64String(((gc '$preB64File' -raw) -replace '\s','')))""")
# N+2. Clean up the temp file.
$cmds.Add("cmd /c del ""$preB64File""")
# N+3 (LAST). Launch the SilverMetal toolbox (run-once).
$cmds.Add("cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command ""Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs""")
$firstLogonSb = New-Object System.Text.StringBuilder
$order = 0
foreach ($cmd in $cmds) {
$order++
[void]$firstLogonSb.AppendLine(" ")
[void]$firstLogonSb.AppendLine(" $order")
[void]$firstLogonSb.AppendLine(" $(EscContent $cmd)")
[void]$firstLogonSb.AppendLine(" ")
}
$firstLogonCommands = $firstLogonSb.ToString().TrimEnd()
@"
$UiLanguage$InputLocale$SystemLocale$UiLanguage$UserLocaleOnError0true1EFI3002MSR163Primarytrue11FAT322233NTFSC03/IMAGE/INDEX1true$InputLocale$SystemLocale$UiLanguage$UiLanguage$UserLocaletruetruetruetruetrue3$unAdministrators$dn$pwtruetrue1$un$pwtrue$cn
$firstLogonCommands
SilverMetalSilverLABS
"@
}