Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 114 additions & 9 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Goal:** Enable quick Windows reinstallation every 2 months without manual reconfiguration
>
> **Last Updated:** 2025-10-02
> **Last Updated:** 2026-04-08
>
> **Note:** Research tasks have been moved to [RESEARCH.md](RESEARCH.md)

Expand Down Expand Up @@ -37,7 +37,7 @@

### Implementation Tasks

- [ ] 🔴 🛠️ Download Sophia Script and add to repository
- [x] 🔴 🛠️ Download Sophia Script and add to repository (implemented as auto-download at runtime via Get-SophiaScript in bootstrap.ps1)
- [x] 🔴 🛠️ Create customized `Sophia.ps1` preset file
- [x] 🟡 🛠️ Document which Sophia options are enabled/disabled
- [x] 🟡 🛠️ Analyze overlap between AutoUnattend.xml and Sophia Script
Expand All @@ -56,7 +56,7 @@
- [x] 🔴 🛠️ Create/obtain base autounattend.xml file
- [x] 🔴 🛠️ Add FirstLogonCommands to execute bootstrap.ps1
- [x] 🟡 🛠️ Configure execution policy bypass in FirstLogonCommands
- [ ] 🟡 🛠️ Add network wait logic before running bootstrap
- [x] 🟡 🛠️ Add network wait logic before running bootstrap (implemented in bootstrap.ps1 via Wait-ForNetwork)

---

Expand All @@ -82,7 +82,7 @@
- [ ] 🔴 🛠️ Add local mode flag (run from repo dir, not C:\Setup)
- [ ] 🔴 🛠️ Add system restore point creation before changes
- [ ] 🔴 🛠️ Add bloat removal path for existing installs (Appx + features)
- [ ] 🔴 🛠️ Auto-download Sophia Script when missing (version-pinned)
- [x] 🔴 🛠️ Auto-download Sophia Script when missing (version-pinned) — implemented via Get-SophiaScript in bootstrap.ps1
- [ ] 🟡 🛠️ Add safety prompt for destructive steps (with -Force override)
- [ ] 🟡 🛠️ Add local-mode logging path (same format as C:\Setup)
- [ ] 🟢 🛠️ Add local-mode desktop shortcut/summary (optional)
Expand Down Expand Up @@ -115,7 +115,7 @@
- [ ] 🟡 🛠️ Create `config/registry.json` structure and schema
- [ ] 🟡 🛠️ Create `config/features.json` for Windows features toggles
- [ ] 🟡 🛠️ Create `config/settings.json` for miscellaneous OS settings
- [ ] 🟢 🛠️ Create PowerShell scripts to apply each config file type
- [x] 🟢 🛠️ Create PowerShell scripts to apply each config file type
- [ ] 🟢 🛠️ Integrate config file application into bootstrap.ps1

---
Expand All @@ -127,7 +127,7 @@
### Implementation Tasks

- [x] 🔴 🛠️ Create `build-iso.ps1` script for automated ISO generation
- [ ] 🔴 🛠️ Add oscdimg.exe downloader (from Windows ADK)
- [ ] 🔴 🛠️ Add oscdimg.exe downloader (from Windows ADK) — Find-OscdImg has download logic but no default URL
- [x] 🔴 🛠️ Implement ISO extraction logic
- [x] 🔴 🛠️ Implement $OEM$ folder structure creation
- [x] 🔴 🛠️ Implement file injection (autounattend.xml, bootstrap.ps1, apps.json, Sophia.ps1)
Expand Down Expand Up @@ -185,7 +185,7 @@
- [x] 🔴 📝 Add Security Warning section to README
- [ ] 🔴 📝 Document how to use build-iso.ps1 (prerequisites, usage, outputs)
- [x] 🟡 📝 Document how to customize Sophia.ps1 preset
- [ ] 🟡 📝 Create docs/ISO-GENERATION.md with detailed ISO creation guide
- [x] 🟡 📝 Create docs/ISO-GENERATION.md with detailed ISO creation guide
- [ ] 🟡 📝 Document $OEM$ folder structure
- [ ] 🟡 📝 Document bootstrap.ps1 log format and location
- [ ] 🟡 📝 Document manual re-run process (desktop shortcut)
Expand Down Expand Up @@ -294,6 +294,93 @@

---

# Phase 4: Infrastructure Improvements

> Structural improvements to enable better testing, modularity, and robustness. These gate all later work.

## Component: Module Refactor (bootstrap.ps1)

> Refactor the ~1300-line monolithic bootstrap.ps1 into reusable PowerShell modules.

### Implementation Tasks

- [ ] 🔴 🛠️ Create `modules/DeclarativeWindows.psm1` root module with common utilities
- [ ] 🔴 🛠️ Extract `Invoke-WinGetInstall` into `modules/WinGet.psm1` (idempotent package install)
- [ ] 🔴 🛠️ Extract `Invoke-SophiaSetup` into `modules/Sophia.psm1` (OS tweaks via Sophia Script)
- [ ] 🔴 🛠️ Extract `Set-DesktopShortcuts` into `modules/Shortcuts.psm1` (lnk creation)
- [ ] 🔴 🛠️ Extract backup/restore logic into `modules/Backup.psm1`
- [ ] 🔴 🛠️ Extract state management into `modules/State.psm1` (Initialize-State, Save-State, Should-RunStep)
- [ ] 🔴 🛠️ Extract registry/application into `modules/Registry.psm1` (PostInstallTweaks, debloat)
- [ ] 🔴 🛠️ Refactor bootstrap.ps1 to import modules and orchestrate (target: <400 lines)
- [ ] 🟡 🛠️ Add module manifest `modules/DeclarativeWindows.psd1`
- [ ] 🟡 🛠️ Add `-WhatIf` support using PowerShell's `SupportsShouldProcess`
- [ ] 🟢 🛠️ Add `--version` flag to bootstrap.ps1 that prints git commit hash

---

## Component: Functional Tests

> Expand Pester tests beyond static string checks to actual behavioral testing with mocking.

### Testing Tasks

- [ ] 🔴 ✅ Add Pester tests for `Find-OscdImg` (mock ADK registry/path, test download fallback)
- [ ] 🔴 ✅ Add Pester tests for `Validate-StagedIsoLayout` with mocked file tree
- [ ] 🔴 ✅ Add Pester tests for `Get-UnattendSetupFileReferences` (parse sample XML)
- [ ] 🔴 ✅ Add Pester tests for bootstrap.ps1 idempotency (run twice, verify state)
- [ ] 🔴 ✅ Add Pester tests for DryRun mode (verify no system changes)
- [ ] 🔴 ✅ Add Pester tests for state management (Initialize-State, Should-RunStep)
- [ ] 🟡 ✅ Add Pester tests for `Invoke-WinGetInstall` with mocked `winget list`
- [ ] 🟡 ✅ Add Pester tests for `Set-DesktopShortcuts` with mocked WScript.Shell
- [ ] 🟢 ✅ Convert existing static tests in BuildIso.Tests.ps1 to functional equivalents

---

## Component: Config File Completion

> Expand stub config files and their apply scripts.

### Implementation Tasks

- [ ] 🟡 🛠️ Populate `config/registry.json` with 10+ commonly-requested registry tweaks
- [ ] 🟡 🛠️ Populate `config/features.json` with Windows features (WSL, SSH, Hyper-V, etc.)
- [ ] 🟡 🛠️ Populate `config/settings.json` with miscellaneous OS settings
- [ ] 🟡 🛠️ Enhance `apply-registry.ps1` to read and apply `config/registry.json`
- [ ] 🟡 🛠️ Create `apply-features.ps1` to enable/disable Windows features from `config/features.json`
- [ ] 🟡 🛠️ Create `apply-settings.ps1` to apply miscellaneous settings from `config/settings.json`
- [ ] 🟢 🛠️ Add JSON schema for each config file type with validation in apply scripts

---

## Component: oscdimg.exe Auto-Downloader

> Complete the auto-downloader in build-iso.ps1 so ADK installation is not required.

### Implementation Tasks

- [ ] 🔴 🛠️ Find reliable source URL for oscdimg.exe (ADK install media or known mirror)
- [ ] 🔴 🛠️ Implement auto-download in `Find-OscdImg` with SHA256 verification
- [ ] 🔴 🛠️ Cache downloaded oscdimg.exe to `$env:LOCALAPPDATA\declarative-windows\tools`
- [ ] 🟡 🛠️ Add `Find-OscdImg` tests with mocked downloads

---

## Component: JSON Schema Validation

> Fail fast on invalid config files before any system changes.

### Implementation Tasks

- [ ] 🟡 🛠️ Download/add WinGet packages.schema.json to `schemas/`
- [ ] 🟡 🛠️ Download Microsoft Unattend.xsd to `schemas/`
- [ ] 🟡 🛠️ Add `Test-AppsJsonSchema` function to validate apps.json against schema
- [ ] 🟡 🛠️ Add `Test-UnattendXmlSchema` function to validate autounattend.xml against XSD
- [ ] 🟡 🛠️ Call schema validation in bootstrap.ps1 before any install operations
- [ ] 🟡 🛠️ Call schema validation in build-iso.ps1 before ISO build
- [ ] 🟢 🛠️ Add Pester tests for schema validation (valid file passes, invalid fails with descriptive error)

---

# Backlog / Future Ideas

> **Research:** See [RESEARCH.md - Backlog](RESEARCH.md#backlog--future-research) for future investigation tasks
Expand All @@ -317,9 +404,27 @@

---

## Implementation Dependency Order

```
Phase 4: Module Refactor ──▶ Functional Tests
│ │
▼ ▼
Config Completion ◀────────── (parallel)
oscdimg Auto-Downloader ──┬──▶ JSON Schema Validation
│ │
▼ ▼
(Phase 2 tasks unlock after Phase 4)
```

---

**Notes:**

- Prioritize Phase 1 MVP tasks to get a working prototype
- Phase 1 MVP tasks remain the primary goal — don't deprioritize them for infrastructure work
- Phase 4 infrastructure gates Phase 2/3 — do these first to unlock later work
- Test frequently in VM to avoid breaking personal system
- Backup current system before testing destructive changes
- Keep security in mind - never commit passwords/tokens to Git
- Keep security in mind never commit passwords/tokens to Git
67 changes: 57 additions & 10 deletions bootstrap.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -498,18 +498,64 @@ function Invoke-WingetManifestInstall {
Write-Log "$ManifestLabel changed since last WinGet run" -Level INFO
}

$tempAppsJson = Join-Path $env:TEMP "apps-missing-$(Get-Random).json"
Write-FilteredAppsJson -AppsData $appsData -PackageIds $missingPackages -OutputPath $tempAppsJson

Write-Log "Installing $($missingPackages.Count) missing packages from $ManifestLabel" -Level INFO
$null = winget import $tempAppsJson --accept-package-agreements --accept-source-agreements 2>&1

$stillMissing = foreach ($packageId in $missingPackages) {
if (-not (Test-WingetPackageInstalled -PackageId $packageId)) {
$packageId
$failedPackages = [System.Collections.Generic.List[string]]::new()

foreach ($packageId in $missingPackages) {
$installed = $false
$scopesToTry = @('user', 'machine')

foreach ($scope in $scopesToTry) {
Write-Log " Installing $packageId (scope: $scope)..." -Level INFO

$stdoutFile = Join-Path $env:TEMP "winget-stdout-$packageId-$(Get-Random).log"
$stderrFile = Join-Path $env:TEMP "winget-stderr-$packageId-$(Get-Random).log"

$proc = Start-Process -FilePath "winget" -ArgumentList "install", $packageId, "--accept-package-agreements", "--accept-source-agreements", "--scope", $scope -NoNewWindow -PassThru -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep winget manifest constraints during package install

Switching from winget import of a filtered manifest to winget install <PackageIdentifier> drops manifest-level constraints (for example source selection and any pinned metadata in exported manifests). In manifests that rely on those fields, installs can come from the wrong source or wrong version, which breaks the declarative/reproducible behavior this step is intended to provide.

Useful? React with 👍 / 👎.

$proc.WaitForExit()

# Stream stdout in real-time
if (Test-Path $stdoutFile) {
Get-Content $stdoutFile | ForEach-Object {
Write-Log " $_" -Level INFO
}
}

# Stream stderr as warnings
if (Test-Path $stderrFile) {
$stderrContent = Get-Content $stderrFile -Raw
if ($stderrContent -and $stderrContent.Trim()) {
Get-Content $stderrFile | ForEach-Object {
Write-Log " $_" -Level WARNING
}
}
}

# Clean up temp output files
if (Test-Path $stdoutFile) { Remove-Item $stdoutFile -Force -ErrorAction SilentlyContinue }
if (Test-Path $stderrFile) { Remove-Item $stderrFile -Force -ErrorAction SilentlyContinue }

# Check if installed
if (Test-WingetPackageInstalled -PackageId $packageId) {
Write-Log " Successfully installed $packageId (scope: $scope)" -Level SUCCESS
$installed = $true
break
}

if ($scope -eq 'user') {
Write-Log " User scope failed for $packageId, trying machine scope..." -Level WARNING
}
}

if (-not $installed) {
$failedPackages.Add($packageId)
Write-Log "WARNING: $packageId failed to install (both user and machine scopes failed)" -Level WARNING
}
}

$stillMissing = $failedPackages

foreach ($packageId in $stillMissing) {
Add-FailedItem -Category $SummaryStep -Item $packageId -Reason "Not installed after import from $ManifestLabel"
Write-Log "WARNING: $packageId still not installed after WinGet import from $ManifestLabel" -Level WARNING
Expand All @@ -530,12 +576,13 @@ function Invoke-WingetManifestInstall {
return $false
}
catch {
Write-Log "ERROR during WinGet import from ${ManifestLabel}: $($_.Exception.Message)" -Level ERROR
Add-SummaryItem -Step $SummaryStep -Status "FAIL" -Message "WinGet import failed"
Set-StepState -StepId $StepId -Status "failed" -Message "WinGet import failed"
Write-Log "ERROR during WinGet install from ${ManifestLabel}: $($_.Exception.Message)" -Level ERROR
Add-SummaryItem -Step $SummaryStep -Status "FAIL" -Message "WinGet install failed"
Set-StepState -StepId $StepId -Status "failed" -Message "WinGet install failed"
return $false
}
finally {
# Legacy single-file cleanup (kept for safety if old path is referenced)
if ($tempAppsJson -and (Test-Path $tempAppsJson)) {
Remove-Item -Path $tempAppsJson -Force -ErrorAction SilentlyContinue
}
Expand Down
18 changes: 16 additions & 2 deletions preflight-backup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ function Normalize-RuleId {
return $normalized.Trim('-').ToLowerInvariant()
}

function Get-RelativePath {
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$BasePath
)
$pathUri = New-Object System.Uri($Path)
$baseUri = New-Object System.Uri($BasePath)
$relativeUri = $baseUri.MakeRelativeUri($pathUri)
$relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())
return $relativePath -replace '/', '\'
}

function Get-BackupRules {
param([object]$Config)

Expand Down Expand Up @@ -339,7 +353,7 @@ foreach ($rule in $rules) {
restorePath = $rule.restorePath
kind = $rule.kind
tags = $rule.tags
backupPath = $ruleDestination
backupPath = Get-RelativePath -Path $ruleDestination -BasePath $sessionRoot
success = $copyResult.Success
message = $copyResult.Message
})
Expand Down Expand Up @@ -371,7 +385,7 @@ foreach ($repoFile in $repoFiles) {
$entry = [ordered]@{
relativePath = $repoFile.relativePath
source = $repoFile.source
backupPath = $destination
backupPath = Get-RelativePath -Path $destination -BasePath $sessionRoot
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve absolute repo backup paths for bootstrap restore

This change writes manifest.repoFiles[*].backupPath as a path relative to the session root, but bootstrap.ps1 still consumes that field as an absolute filesystem path in Restore-RepoFilesFromManifest (Test-Path $repoFile.backupPath then Copy-Item -Path $repoFile.backupPath). In the first-logon flow, that makes personal repo files (like apps.json / config\backup.json) fail to restore from new backups because the relative path is resolved from the wrong working directory.

Useful? React with 👍 / 👎.

}

if ($VerifyHashes -and (Test-Path $destination)) {
Expand Down
Loading
Loading