diff --git a/windows/collector/New-SmAnswerFile.ps1 b/windows/collector/New-SmAnswerFile.ps1
new file mode 100644
index 0000000..d0cc6a2
--- /dev/null
+++ b/windows/collector/New-SmAnswerFile.ps1
@@ -0,0 +1,105 @@
+#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 a
+# specialize-pass command writes to C:\ProgramData\SilverMetal\preconfig.json.
+
+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
+
+ $writePre = "powershell -NoProfile -ExecutionPolicy Bypass -Command "" New-Item -ItemType Directory -Force 'C:\ProgramData\SilverMetal' | Out-Null; [IO.File]::WriteAllBytes('C:\ProgramData\SilverMetal\preconfig.json', [Convert]::FromBase64String('$preB64')) """
+
+@"
+
+
+
+
+ $UiLanguage
+ $InputLocale$SystemLocale
+ $UiLanguage$UserLocale
+
+
+
+ OnError
+
+ 0true
+
+ 1EFI300
+ 2MSR16
+ 3Primarytrue
+
+
+ 11FAT32
+ 22
+ 33NTFSC
+
+
+
+
+ 03
+ /IMAGE/INDEX1
+
+ true
+
+
+
+
+
+
+ 1
+ $(EscContent $writePre)
+ Write SilverMetal preconfig
+
+
+
+
+
+
+ $InputLocale$SystemLocale
+ $UiLanguage$UiLanguage$UserLocale
+
+
+ truetruetruetruetrue3
+
+
+ $unAdministrators$dn
+ $pwtrue
+
+
+ true1$un$pwtrue
+ $cn
+
+
+ 1
+ cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs"
+ Launch SilverMetal toolbox (run-once)
+
+
+ SilverMetalSilverLABS
+
+
+
+"@
+}
diff --git a/windows/tests/Collector.Tests.ps1 b/windows/tests/Collector.Tests.ps1
index 8a84196..b4ef7e2 100644
--- a/windows/tests/Collector.Tests.ps1
+++ b/windows/tests/Collector.Tests.ps1
@@ -30,3 +30,40 @@ Describe 'Test-SmComputerName' {
It 'rejects > 15 chars' { (Test-SmComputerName ('A'*16)).Ok | Should -BeFalse }
It 'rejects illegal chars' { (Test-SmComputerName 'bad name').Ok | Should -BeFalse }
}
+
+Describe 'New-SmAnswerFile' {
+ BeforeAll {
+ . (Join-Path $PSScriptRoot '..\collector\New-SmAnswerFile.ps1')
+ $cfg = @{
+ DisplayName = 'Jamie'; Username = 'jamie'; Password = 'Sup3rPass!'
+ ComputerName = 'SILVER-01'
+ InputLocale = '0809:00000809'; SystemLocale = 'en-GB'; UiLanguage = 'en-US'; UserLocale = 'en-GB'
+ Flavour = 'developer'; BitLockerEnable = $true; BitLockerPin = '246810'
+ }
+ $script:xml = New-SmAnswerFile @cfg
+ }
+ It 'is valid XML' { { [xml]$script:xml } | Should -Not -Throw }
+ It 'creates the real account in Administrators' {
+ $script:xml | Should -Match 'jamie'
+ $script:xml | Should -Match 'Administrators'
+ }
+ It 'does NOT contain sm-bootstrap' { $script:xml | Should -Not -Match 'sm-bootstrap' }
+ It 'sets AutoLogon once as the user' {
+ $script:xml | Should -Match '1'
+ $script:xml | Should -Match 'jamie'
+ }
+ It 'sets the computer name' { $script:xml | Should -Match 'SILVER-01' }
+ It 'keeps WillWipeDisk for disk 0' { $script:xml | Should -Match 'true' }
+ It 'embeds a base64 preconfig write in specialize' {
+ $script:xml | Should -Match 'preconfig\.json'
+ $script:xml | Should -Match 'FromBase64String'
+ }
+ It 'embedded preconfig round-trips with the flavour and pin' {
+ $m = [regex]::Match($script:xml, "FromBase64String\('([^']+)'\)")
+ $m.Success | Should -BeTrue
+ $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($m.Groups[1].Value)) | ConvertFrom-Json
+ $json.flavour | Should -Be 'developer'
+ $json.bitlocker.pin | Should -Be '246810'
+ }
+ It 'launches the toolbox in FirstLogonCommands' { $script:xml | Should -Match 'SilverOS\.Welcome\.App\.exe' }
+}