diff --git a/windows/collector/New-SmAnswerFile.ps1 b/windows/collector/New-SmAnswerFile.ps1
index d0cc6a2..06b8f89 100644
--- a/windows/collector/New-SmAnswerFile.ps1
+++ b/windows/collector/New-SmAnswerFile.ps1
@@ -1,8 +1,11 @@
#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.
+# 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(
@@ -29,7 +32,45 @@ function New-SmAnswerFile {
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')) """
+ # 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()
@"
@@ -64,17 +105,6 @@ function New-SmAnswerFile {
true
-
-
-
-
- 1
- $(EscContent $writePre)
- Write SilverMetal preconfig
-
-
-
-
$InputLocale$SystemLocale
@@ -91,11 +121,7 @@ function New-SmAnswerFile {
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)
-
+$firstLogonCommands
SilverMetalSilverLABS
diff --git a/windows/tests/Collector.Tests.ps1 b/windows/tests/Collector.Tests.ps1
index b4ef7e2..84f4d03 100644
--- a/windows/tests/Collector.Tests.ps1
+++ b/windows/tests/Collector.Tests.ps1
@@ -54,16 +54,19 @@ Describe 'New-SmAnswerFile' {
}
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
+ It 'preconfig round-trips via chunked FirstLogonCommands' {
+ # Gather the echo'd base64 chunks in Order, concatenate, strip whitespace, decode.
+ $chunks = [regex]::Matches($script:xml, 'echo ([A-Za-z0-9+/=]+)') | ForEach-Object { $_.Groups[1].Value }
+ $b64 = ($chunks -join '')
+ $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b64)) | ConvertFrom-Json
$json.flavour | Should -Be 'developer'
$json.bitlocker.pin | Should -Be '246810'
}
+ It 'has no specialize pass anymore' { $script:xml | Should -Not -Match 'pass="specialize"' }
+ It 'creates the preconfig dir + decodes it at first logon' {
+ $script:xml | Should -Match 'ProgramData\\SilverMetal'
+ $script:xml | Should -Match 'FromBase64String'
+ $script:xml | Should -Match 'preconfig\.json'
+ }
It 'launches the toolbox in FirstLogonCommands' { $script:xml | Should -Match 'SilverOS\.Welcome\.App\.exe' }
}