From b2f4d6310e176ada0211042bfd95ff8f4f704d23 Mon Sep 17 00:00:00 2001 From: Vasyl Palamarchuk Date: Wed, 9 Sep 2026 11:46:36 -0700 Subject: [PATCH] Update summary. --- Automotive-Workstation-Setup.ps1 | 269 +++++++++++++++++++++++++++++-- COMPLETION-SUMMARY.md | 165 +++++++++---------- Config/windows-settings.json | 38 +++++ Config/workspace-templates.json | 56 +++++++ TROUBLESHOOTING.md | 38 +++++ Tests/Configuration.Tests.ps1 | 37 +++++ Tests/ExecutionModes.Tests.ps1 | 40 +++++ Tests/Run-AllTests.ps1 | 16 ++ Tests/Workspace.Tests.ps1 | 54 +++++++ USER-GUIDE.md | 61 +++++++ 10 files changed, 672 insertions(+), 102 deletions(-) create mode 100644 Config/windows-settings.json create mode 100644 Config/workspace-templates.json create mode 100644 TROUBLESHOOTING.md create mode 100644 Tests/Configuration.Tests.ps1 create mode 100644 Tests/ExecutionModes.Tests.ps1 create mode 100644 Tests/Run-AllTests.ps1 create mode 100644 Tests/Workspace.Tests.ps1 create mode 100644 USER-GUIDE.md diff --git a/Automotive-Workstation-Setup.ps1 b/Automotive-Workstation-Setup.ps1 index 786a2af..0d0b8bb 100644 --- a/Automotive-Workstation-Setup.ps1 +++ b/Automotive-Workstation-Setup.ps1 @@ -67,6 +67,8 @@ param( [string]$SharedPortableLabel = 'PORTABLE_APPS', [ValidateSet('DailyTech & Tuning', 'ODIS & XENTRY', 'PIWIS & ISTA')] [string]$WorkstationProfile = 'DailyTech & Tuning', + [ValidateSet('Apply', 'Audit', 'Plan', 'Repair', 'HealthCheck', 'Backup', 'Restore', 'Inventory', 'UpdatePortable', 'CreateWorkspace')] + [string]$Mode = 'Apply', [switch]$InstallOptionalApps, [switch]$CreateSharedLinks = $true, [switch]$SkipDownloads, @@ -82,7 +84,16 @@ param( [ValidateRange(30, 3600)][int]$DownloadTimeoutSeconds = 900, [switch]$EnableTranscript, [string]$ExecutionId, - [switch]$ResumeFromCheckpoint + [switch]$ResumeFromCheckpoint, + [string]$JobId, + [string]$Manufacturer, + [string]$Model, + [ValidateRange(1886, 2100)][int]$ModelYear = (Get-Date).Year, + [ValidateSet('ECU', 'TCU', 'EEPROM', 'ABS', 'Other')] + [string]$Module = 'ECU', + [string]$VIN, + [string]$Notes, + [string]$Technician ) Set-StrictMode -Version Latest @@ -112,6 +123,8 @@ $Configuration = @{ Shortcuts = Import-SetupConfiguration -Name 'shortcuts.json' Profiles = Import-SetupConfiguration -Name 'workstation-profiles.json' Automotive = Import-SetupConfiguration -Name 'automotive-resources.json' + WindowsSettings = Import-SetupConfiguration -Name 'windows-settings.json' + WorkspaceTemplates = Import-SetupConfiguration -Name 'workspace-templates.json' } function Get-ProfileCatalogItems { @@ -131,6 +144,15 @@ if ($Profile.Count -ne 1) { throw "Workstation profile '$WorkstationProfile' is not defined in Config\workstation-profiles.json." } $Profile = $Profile[0] +$EffectiveMode = $Mode +if ($HealthCheck) { $EffectiveMode = 'HealthCheck' } +elseif ($BackupConfiguration) { $EffectiveMode = 'Backup' } +elseif ($RestoreConfiguration) { $EffectiveMode = 'Restore' } +$windowsProfileKey = $Profile.Folder +if (-not $Configuration.WindowsSettings.Profiles.PSObject.Properties[$windowsProfileKey]) { + throw "Windows settings are not defined for profile '$windowsProfileKey'." +} +$DesiredWindowsSettings = $Configuration.WindowsSettings.Profiles.$windowsProfileKey $ProfileDataRoot = "{0}\Workstations\{1}" -f $SharedDataRoot.TrimEnd('\'), $Profile.Folder $ProfileConfigRoot = "{0}\Config\{1}" -f $SharedPortableRoot.TrimEnd('\'), $Profile.Folder $ProfileInstallerRoot = Join-Path $SharedPortableRoot (Join-Path 'Install' $Profile.Folder) @@ -174,7 +196,7 @@ $Events = [System.Collections.Generic.List[object]]::new() $ExecutionState = [ordered]@{ ExecutionId = $ExecutionId Profile = $WorkstationProfile - Mode = 'Apply' + Mode = $EffectiveMode StartTime = $ScriptStartTime.ToString('o') LastCheckpoint = $null Status = 'InProgress' @@ -442,6 +464,170 @@ function Test-Administrator { return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } +function Invoke-PowerCfg { + param([Parameter(Mandatory)][string[]]$Arguments) + $process = Start-Process -FilePath 'powercfg.exe' -ArgumentList $Arguments -Wait -PassThru -NoNewWindow + if ($process.ExitCode -ne 0) { + throw "powercfg.exe $($Arguments -join ' ') failed with exit code $($process.ExitCode)." + } +} + +function Get-WindowsConfigurationState { + $activeScheme = (& powercfg.exe /getactivescheme 2>$null) -join ' ' + $powerSettings = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue + $hibernateValue = if ($null -eq $powerSettings -or -not $powerSettings.PSObject.Properties['HibernateEnabled']) { $null } else { $powerSettings.HibernateEnabled } + $fileSystem = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -ErrorAction SilentlyContinue + $explorer = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -ErrorAction SilentlyContinue + return [pscustomobject]@{ + Time = Get-Date + ActivePowerScheme = $activeScheme + HibernateEnabled = $hibernateValue + UsbSelectiveSuspend = 'Inspect with powercfg /query' + MonitorTimeoutAC = 'Inspect with powercfg /query' + DiskTimeoutAC = 'Inspect with powercfg /query' + StandbyTimeoutAC = 'Inspect with powercfg /query' + ShowFileExtensions = if ($null -eq $explorer -or -not $explorer.PSObject.Properties['HideFileExt']) { $null } else { $explorer.HideFileExt -eq 0 } + LongPathsEnabled = if ($null -eq $fileSystem -or -not $fileSystem.PSObject.Properties['LongPathsEnabled']) { $null } else { $fileSystem.LongPathsEnabled -eq 1 } + } +} + +function Get-WindowsConfigurationPlan { + $current = Get-WindowsConfigurationState + $desired = $DesiredWindowsSettings + return @( + [pscustomobject]@{ Setting='PowerPlan'; Current=$current.ActivePowerScheme; Desired=$desired.PowerPlan; Action='Set active scheme' } + [pscustomobject]@{ Setting='Hibernation'; Current=if ($current.HibernateEnabled -eq 1) { 'Enabled' } else { 'Disabled' }; Desired=$desired.Hibernation; Action='powercfg /hibernate' } + [pscustomobject]@{ Setting='USB selective suspend'; Current=$current.UsbSelectiveSuspend; Desired=$desired.UsbSelectiveSuspend; Action='Set AC/DC USB policy' } + [pscustomobject]@{ Setting='Monitor timeout AC'; Current=$current.MonitorTimeoutAC; Desired="$($desired.MonitorTimeoutACMinutes) minutes"; Action='powercfg /change' } + [pscustomobject]@{ Setting='System sleep AC'; Current=$current.StandbyTimeoutAC; Desired="$($desired.StandbyTimeoutACMinutes) minutes"; Action='powercfg /change' } + [pscustomobject]@{ Setting='Show file extensions'; Current=$current.ShowFileExtensions; Desired=[bool]$desired.ShowFileExtensions; Action='Explorer registry setting' } + [pscustomobject]@{ Setting='Long paths'; Current=$current.LongPathsEnabled; Desired=[bool]$desired.LongPathsEnabled; Action='FileSystem registry setting' } + ) +} + +function Set-WindowsProfileConfiguration { + $desired = $DesiredWindowsSettings + if ($desired.Hibernation -eq 'Disabled') { Invoke-PowerCfg -Arguments @('/hibernate', 'off') } + else { Invoke-PowerCfg -Arguments @('/hibernate', 'on') } + + $usbValue = if ($desired.UsbSelectiveSuspend -eq 'Disabled') { '0' } else { '1' } + Invoke-PowerCfg -Arguments @('/setacvalueindex', 'SCHEME_CURRENT', 'SUB_USB', 'USBSELECTIVE', $usbValue) + Invoke-PowerCfg -Arguments @('/setdcvalueindex', 'SCHEME_CURRENT', 'SUB_USB', 'USBSELECTIVE', $usbValue) + Invoke-PowerCfg -Arguments @('/setactive', $desired.PowerPlanAlias) + Invoke-PowerCfg -Arguments @('/change', 'monitor-timeout-ac', [string]$desired.MonitorTimeoutACMinutes) + Invoke-PowerCfg -Arguments @('/change', 'disk-timeout-ac', [string]$desired.DiskTimeoutACMinutes) + Invoke-PowerCfg -Arguments @('/change', 'standby-timeout-ac', [string]$desired.StandbyTimeoutACMinutes) + + $explorerPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' + $hideFileExtensionValue = if ($desired.ShowFileExtensions) { 0 } else { 1 } + Set-ItemProperty -Path $explorerPath -Name HideFileExt -Value $hideFileExtensionValue + $fileSystemPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' + $longPathValue = if ($desired.LongPathsEnabled) { 1 } else { 0 } + Set-ItemProperty -Path $fileSystemPath -Name LongPathsEnabled -Value $longPathValue + Write-Log "Applied Windows settings for $($Profile.Folder): $($desired.PowerPlan), hibernation $($desired.Hibernation), USB suspend $($desired.UsbSelectiveSuspend)." 'OK' +} + +function ConvertTo-ProjectSegment { + param([Parameter(Mandatory)][string]$Value, [Parameter(Mandatory)][string]$Name) + $segment = $Value.Trim() + if ([string]::IsNullOrWhiteSpace($segment) -or $segment -match '[\\/:*?"<>|]' -or $segment -eq '.' -or $segment -eq '..') { + throw "$Name contains an invalid path segment: '$Value'." + } + return ($segment -replace '\s+', '_') +} + +function New-AutomotiveProject { + param( + [Parameter(Mandatory)][string]$ProjectJobId, + [Parameter(Mandatory)][string]$ProjectManufacturer, + [Parameter(Mandatory)][string]$ProjectModel, + [int]$ProjectModelYear = (Get-Date).Year, + [string]$ProjectVIN = '', + [string]$ProjectNotes = '', + [string]$ProjectTechnician = '' + ) + $templateProperty = $Configuration.WorkspaceTemplates.Templates.PSObject.Properties[$Profile.Folder] + if (-not $templateProperty) { throw "Workspace template is not defined for profile '$($Profile.Folder)'." } + $template = $templateProperty.Value + + $safeJobId = ConvertTo-ProjectSegment -Value $ProjectJobId -Name 'JobId' + $safeManufacturer = ConvertTo-ProjectSegment -Value $ProjectManufacturer -Name 'Manufacturer' + $safeModel = ConvertTo-ProjectSegment -Value $ProjectModel -Name 'Model' + $date = Get-Date -Format 'yyyy-MM-dd' + $projectName = '{0}_{1}_{2}_{3}' -f $safeJobId, $safeManufacturer, $safeModel, $date + $localBase = Join-Path $LocalRoot $template.LocalBase + $localProject = Join-Path $localBase $projectName + $sharedProject = Join-Path $ProfileDataRoot $projectName + + $localFolders = @($template.Folders | ForEach-Object { Join-Path $localProject $_ }) + $sharedFolders = @($template.Folders | ForEach-Object { Join-Path $sharedProject $_ }) + $null = New-Item -ItemType Directory -Path @($localProject, $sharedProject) -Force + $null = New-Item -ItemType Directory -Path ($localFolders + $sharedFolders) -Force + + $now = (Get-Date).ToString('o') + $metadata = [ordered]@{ + SchemaVersion = '1.0' + JobId = $safeJobId + Profile = $Profile.Folder + CreatedDate = $now + LastModified = $now + Vehicle = [ordered]@{ + Manufacturer = $safeManufacturer + Model = $safeModel + ModelYear = $ProjectModelYear + VIN = $ProjectVIN + } + Module = [ordered]@{ Type = $Module; PartNumber = ''; HardwareVersion = ''; SoftwareVersion = '' } + Work = [ordered]@{ + OperationType = $template.OperationType + Technician = $ProjectTechnician + TechnicianNotes = $ProjectNotes + AuthorizationReference = '' + ToolsUsed = @() + } + Paths = [ordered]@{ Local = $localProject; Shared = $sharedProject } + Status = 'New' + } + $metadataPath = Join-Path $localProject 'ProjectMetadata.json' + $metadata | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $metadataPath -Encoding UTF8 + $metadata | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $sharedProject 'ProjectMetadata.json') -Encoding UTF8 + + $jobLog = @" +# $safeJobId - $safeManufacturer $safeModel + +**Profile:** $($Profile.Folder) +**Created:** $now +**Module:** $Module +**Technician:** $ProjectTechnician +**VIN:** $ProjectVIN + +## Work Notes +$ProjectNotes + +## Timeline + +### $now - Project initialized +- Workspace folders created from the $($Profile.Folder) template. +- Original vehicle/module files belong in the `Originals` or profile-equivalent folder. +- Preserve originals and record SHA256 hashes before making changes. + +## Recovery +- Local project: `$localProject` +- Shared project copy: `$sharedProject` +- Do not flash, code, erase, or modify a vehicle without explicit authorization and verified backups. +"@ + $jobLog | Set-Content -LiteralPath (Join-Path $localProject 'JobLog.md') -Encoding UTF8 + $jobLog | Set-Content -LiteralPath (Join-Path $sharedProject 'JobLog.md') -Encoding UTF8 + + return [pscustomobject]@{ + JobId = $safeJobId + Profile = $Profile.Folder + LocalPath = $localProject + SharedPath = $sharedProject + MetadataPath = $metadataPath + } +} + function New-Shortcut { param( [Parameter(Mandatory)][string]$ShortcutPath, @@ -835,6 +1021,7 @@ Write-Log "Local root: $LocalRoot" Write-Log "Shared data root: $SharedDataRoot" Write-Log "Shared portable root: $SharedPortableRoot" Write-Log "Execution ID: $ExecutionId" +Write-Log "Execution mode: $EffectiveMode" if ($ResumeFromCheckpoint) { $checkpoint = Load-ExecutionCheckpoint @@ -849,33 +1036,81 @@ if ($EnableTranscript) { Start-Transcript -LiteralPath $transcriptPath -Append | Out-Null } -if ($HealthCheck) { +if ($EffectiveMode -eq 'Audit' -or $EffectiveMode -eq 'HealthCheck') { Invoke-Safe 'HealthCheck' 'Automotive workstation health check' { Test-WorkstationHealth | Out-Null } | Out-Null + if ($EffectiveMode -eq 'Audit') { + Invoke-Safe 'Inventory' 'Export workstation inventory' { Export-WorkstationInventory } | Out-Null + } $Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8 if ($EnableTranscript) { Stop-Transcript | Out-Null } return } +if ($EffectiveMode -eq 'Plan') { + $windowsPlan = @(Get-WindowsConfigurationPlan) + $planFile = Join-Path $Paths.Logs ("{0}_Plan_{1}.csv" -f $Profile.Folder, $TimeStamp) + $windowsPlan | Export-Csv -LiteralPath $planFile -NoTypeInformation -Encoding UTF8 + $catalogPlan = if ($Configuration.AppCatalog) { + @($Configuration.AppCatalog.Packages | Where-Object { + (-not $_.PSObject.Properties['Profiles']) -or @($_.Profiles) -contains $Profile.Folder + } | Select-Object Name, Id, Channel) + } else { + @($Configuration.CorePackages | Select-Object Name, Id | ForEach-Object { $_ | Add-Member -NotePropertyName Channel -NotePropertyValue 'Core' -PassThru }) + } + $catalogPlan | Export-Csv -LiteralPath (Join-Path $Paths.Logs ("{0}_ApplicationPlan_{1}.csv" -f $Profile.Folder, $TimeStamp)) -NoTypeInformation -Encoding UTF8 + Write-Log "Plan generated for $($Profile.Folder): $($catalogPlan.Count) catalog applications and $($windowsPlan.Count) Windows settings." 'OK' + $Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8 + if ($EnableTranscript) { Stop-Transcript | Out-Null } + return +} + +if ($EffectiveMode -eq 'CreateWorkspace') { + if ([string]::IsNullOrWhiteSpace($JobId) -or [string]::IsNullOrWhiteSpace($Manufacturer) -or [string]::IsNullOrWhiteSpace($Model)) { + throw '-Mode CreateWorkspace requires -JobId, -Manufacturer, and -Model.' + } + $project = New-AutomotiveProject -ProjectJobId $JobId -ProjectManufacturer $Manufacturer ` + -ProjectModel $Model -ProjectModelYear $ModelYear -ProjectVIN $VIN ` + -ProjectNotes $Notes -ProjectTechnician $Technician + Add-Result 'Workspace' $project.JobId 'Success' $project.LocalPath + $project | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $Paths.Logs "Workspace_$TimeStamp.json") -Encoding UTF8 + Write-Log "Workspace created: $($project.LocalPath)" 'OK' + $Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8 + if ($EnableTranscript) { Stop-Transcript | Out-Null } + return +} + +if ($EffectiveMode -eq 'UpdatePortable') { + $SkipWinget = $true + $InstallOptionalApps = $false +} + if (-not (Test-Administrator)) { throw 'Run this script from Windows PowerShell or PowerShell as Administrator.' } +if ($EffectiveMode -eq 'Backup') { + Invoke-Safe 'Recovery' 'Backup workstation configuration' { + $archive = Backup-WorkstationConfiguration + Write-Log "Configuration backup created: $archive" 'OK' + } | Out-Null + $Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8 + if ($EnableTranscript) { Stop-Transcript | Out-Null } + return +} + +if ($EffectiveMode -eq 'Restore') { + Invoke-Safe 'Recovery' 'Restore workstation configuration' { Restore-WorkstationConfiguration } | Out-Null + $Results | Export-Csv -LiteralPath $SummaryFile -NoTypeInformation -Encoding UTF8 + if ($EnableTranscript) { Stop-Transcript | Out-Null } + return +} + Save-ExecutionCheckpoint -Phase 'Initialize' -Status 'InProgress' -Details 'Prerequisites and administrator check passed.' -Invoke-Safe 'Configuration' 'Disable hibernation' { - $powerProcess = Start-Process -FilePath 'powercfg.exe' -ArgumentList '/hibernate', 'off' -Wait -PassThru -NoNewWindow - if ($powerProcess.ExitCode -ne 0) { throw "powercfg.exe /hibernate off failed with exit code $($powerProcess.ExitCode)." } -} | Out-Null - -Invoke-Safe 'Configuration' 'Disable USB selective suspend' { - foreach ($powerArguments in @( - @('/setacvalueindex', 'SCHEME_CURRENT', 'SUB_USB', 'USBSELECTIVE', '0'), - @('/setdcvalueindex', 'SCHEME_CURRENT', 'SUB_USB', 'USBSELECTIVE', '0'), - @('/setactive', 'SCHEME_CURRENT') - )) { - $powerProcess = Start-Process -FilePath 'powercfg.exe' -ArgumentList $powerArguments -Wait -PassThru -NoNewWindow - if ($powerProcess.ExitCode -ne 0) { throw "powercfg.exe $($powerArguments -join ' ') failed with exit code $($powerProcess.ExitCode)." } - } +Invoke-Safe 'Configuration' "Apply Windows settings for $($Profile.Folder)" { + Set-WindowsProfileConfiguration + $windowsState = Get-WindowsConfigurationState + $windowsState | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $Paths.Config "WindowsState_$TimeStamp.json") -Encoding UTF8 } | Out-Null if ($RestoreConfiguration) { diff --git a/COMPLETION-SUMMARY.md b/COMPLETION-SUMMARY.md index d09e699..a3593fd 100644 --- a/COMPLETION-SUMMARY.md +++ b/COMPLETION-SUMMARY.md @@ -1,8 +1,8 @@ # PROJECT COMPLETION SUMMARY ## Automotive Workstation Setup - 9 Phase Audit & Redesign -**Project Completion:** ✅ 2026-09-09 -**Status:** All 9 phases complete and documented +**Project Update:** ✅ 2026-09-09 +**Status:** Design complete; incremental implementation completed through Week 5 --- @@ -63,15 +63,23 @@ - Example test implementations - Code coverage expectations -9. **PHASE-9-IMPLEMENTATION.md** (40 hours planned) +9. **PHASE-9-IMPLEMENTATION.md** (implementation roadmap) - Complete script refactoring roadmap - 8 modular PowerShell modules - 14 JSON configuration files (10 new, 4 legacy) - All 25 issues resolution mapping - Migration guide and deployment checklist -### 📊 Master Index & Navigation -- **00-PROJECT-MASTER-INDEX.md** — Complete project overview with navigation +### Implemented Work +- [Automotive-Workstation-Setup.ps1](Automotive-Workstation-Setup.ps1) — Weeks 1-4 implementation +- [Config/app-catalog.json](Config/app-catalog.json) — Profile-aware application catalog +- [Config/windows-settings.json](Config/windows-settings.json) — Profile-specific Windows policy +- [Config/workspace-templates.json](Config/workspace-templates.json) — Project workspace templates +- [Tests/Run-AllTests.ps1](Tests/Run-AllTests.ps1) — Pester test runner +- [USER-GUIDE.md](USER-GUIDE.md) and [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — Week 5 documentation + +### Master Index & Navigation +- [00-PROJECT-MASTER-INDEX.md](00-PROJECT-MASTER-INDEX.md) — Complete project overview with navigation --- @@ -85,23 +93,30 @@ | Medium Severity | 12/12 | ✅ All resolved | | **Total** | **25/25** | **✅ 100%** | -### Features Designed -- ✅ 10 execution modes with distinct workflows -- ✅ Checkpoint-based recovery with restart support -- ✅ 7-layer download validation framework -- ✅ Profile-specific application filtering -- ✅ Automated workspace/project initialization -- ✅ Complete Windows OS configuration per profile -- ✅ Comprehensive health checking and reporting -- ✅ Full backup/restore capability -- ✅ Modular, testable architecture +### Features Implemented +- ✅ Profile-aware application catalog and VS Code extension filtering +- ✅ Parameter-driven installer paths and execution checkpoints +- ✅ HTTPS enforcement with explicit documented HTTP exceptions +- ✅ Portable download size, optional SHA256, archive, and Authenticode checks +- ✅ Profile-specific Windows power and file-system configuration +- ✅ Audit, Plan, Apply, HealthCheck, Backup, Restore, UpdatePortable, and CreateWorkspace paths +- ✅ Automated local/shared workspace creation with metadata and job logs +- ✅ Health, inventory, backup, restore, and structured reporting functions +- ✅ Pester 6 test runner with 10 passing tests + +### Remaining Design-Level Work +- ⏳ Full modular split into eight `.psm1` modules +- ⏳ Malware scanning integration and mandatory trusted-hash catalog population +- ⏳ Automatic restart prompting and phase skipping during resume +- ⏳ Dedicated Repair and Inventory dispatch workflows +- ⏳ Full 70+ test expansion described by PHASE-8 ### Coverage Provided - ✅ 9 detailed phase documents (equivalent of 100+ pages) -- ✅ 14 JSON configuration schemas -- ✅ 70+ test specifications -- ✅ Complete API reference (80+ functions planned) -- ✅ User guides, troubleshooting, migration path +- ✅ 10 JSON configuration files, including application, Windows, and workspace catalogs +- ✅ 10 executable Pester tests across configuration, modes, and workspaces +- ✅ User guide and troubleshooting guide +- ✅ Validation checklist and migration/design documentation --- @@ -110,13 +125,13 @@ | Aspect | Current | New System | |--------|---------|-----------| | **Execution Modes** | 1 (Apply only) | 10 (Audit, Plan, Apply, Repair, HealthCheck, Backup, Restore, Inventory, UpdatePortable, CreateWorkspace) | -| **Restart Handling** | Manual/breaks script | Automatic via checkpoint system | -| **Download Validation** | Minimal (existence only) | 7-layer framework (HTTPS, size, archive, SHA256, signature, malware) | +| **Restart Handling** | Manual/breaks script | Checkpoint persistence and explicit resume path | +| **Download Validation** | Minimal (existence only) | HTTPS, size, archive, optional SHA256, and Authenticode checks | | **Error Recovery** | Requires manual rerun | Automated via Repair mode + checkpoints | | **Profile Differentiation** | None (identical apps) | Full (AllowList/DenyList per profile) | | **Workspace Support** | Manual folder creation | Automated templates with metadata | | **Windows Config** | Hard-coded values | Profile-specific, reversible, testable | -| **Testing** | Ad-hoc manual | 70+ automated Pester tests | +| **Testing** | Ad-hoc manual | 10 automated Pester tests, with expansion planned | | **Logging** | Minimal | Comprehensive audit trail + structured logs | | **Documentation** | Sparse | 9 guides (100+ pages equivalent) | @@ -144,39 +159,39 @@ --- -## 10 EXECUTION MODES AT A GLANCE +## EXECUTION MODES ``` -Audit Mode → Scan current state (no changes) -Plan Mode → Show what will be installed/changed -Apply Mode → Full installation with checkpoint recovery -Repair Mode → Fix failed installs from previous runs -HealthCheck Mode → Verify all installations are correct -Backup Mode → Save current state for rollback -Restore Mode → Rollback to previous state -Inventory Mode → Export list of installed applications -UpdatePortable Mode → Update portable apps to latest versions -CreateWorkspace → Initialize new project with templates +Audit Mode → Scan current state (no setup changes) +Plan Mode → Generate Windows and application plans +Apply Mode → Full installation with checkpoint persistence +Repair Mode → Accepted mode; currently follows idempotent apply behavior +HealthCheck Mode → Verify workstation health +Backup Mode → Save current configuration +Restore Mode → Restore the latest configuration backup +Inventory Mode → Inventory support through the audit/reporting path +UpdatePortable Mode → Run portable-app flow without WinGet +CreateWorkspace → Initialize a profile-specific project ``` --- ## CHECKPOINT & RESTART RECOVERY -The new system can **resume after a forced restart** (WinGet often requires reboot): +The current system persists execution checkpoints and can be explicitly resumed after a restart: 1. **Execution starts** → Create checkpoint with ExecutionId 2. **Phase completes** → Update checkpoint, log progress 3. **Restart detected** → Save state to disk, prompt user 4. **System reboots** → Checkpoint persisted, waiting for resume -5. **Script resumes** → Load checkpoint, skip completed phases -6. **Continue from where it left off** → Full transparency and recovery +5. **Script resumes** → Load checkpoint and revalidate prior work +6. **Continue safely** → Idempotent installers continue from the saved execution context --- -## SECURITY FRAMEWORK (7 LAYERS) +## DOWNLOAD SECURITY -Every download validates through: +The implementation currently validates downloads through: 1. **HTTPS Enforcement** — Encrypted, prevents man-in-the-middle 2. **File Size Check** — Detects truncation or injection @@ -184,42 +199,23 @@ Every download validates through: 4. **Expected Files** — Verifies correct files extracted 5. **SHA256 Hash** — Cryptographic integrity verification 6. **Authenticode** — Digital signature on executables -7. **Malware Scan** — Windows Defender scan (optional) +7. **Malware Scan** — Reserved for the planned security expansion HTTP allowed only on documented allow-list with strong justification. --- -## NEXT STEPS FOR IMPLEMENTATION +## NEXT STEPS -### Week 1-2: Core Infrastructure -- [ ] Refactor script parameters and configuration loading -- [ ] Implement checkpoint/recovery system -- [ ] Create logging framework -- [ ] Fix critical path derivation bug -- [ ] Add admin privilege check - -### Week 2-3: Applications & Validation -- [ ] Create unified app-catalog.json -- [ ] Implement WinGet installation with retry -- [ ] Implement 7-layer download validation -- [ ] Fix MVCI PRO duplication -- [ ] Test on first profile - -### Week 3-4: Features & Modes -- [ ] Implement all 10 execution modes -- [ ] Create Windows configuration functions -- [ ] Implement workspace templates -- [ ] Add HealthCheck and Repair logic -- [ ] Create backup/restore functionality - -### Week 4-5: Testing & Deployment -- [ ] Run full Pester test suite (70+ tests) -- [ ] Test restart recovery thoroughly -- [ ] Complete user documentation -- [ ] Deploy to test workstations -- [ ] Gather feedback and refine -- [ ] Deploy to production +### Remaining implementation +- [ ] Split the monolithic script into the planned PowerShell modules +- [ ] Complete dedicated Repair and Inventory mode dispatch +- [ ] Add automatic restart prompting and phase skipping +- [ ] Populate and enforce trusted SHA256 values for portable releases +- [ ] Add Windows Defender malware scanning integration +- [ ] Expand the Pester suite toward the PHASE-8 target +- [ ] Run elevated Apply tests on clean profile workstations +- [ ] Complete deployment packaging and production rollout --- @@ -249,7 +245,8 @@ HTTP allowed only on documented allow-list with strong justification. - **Execution Modes:** 10 - **Application Profiles:** 3 - **Applications Cataloged:** 65+ -- **Tests Specified:** 70+ +- **Tests Implemented:** 10 passing Pester tests +- **Tests Planned:** 70+ - **JSON Schemas:** 14 - **Functions Designed:** 80+ @@ -258,9 +255,9 @@ HTTP allowed only on documented allow-list with strong justification. ## QUALITY METRICS ### Coverage -- **Code Coverage (Target):** 87.5% +- **Code Coverage:** Not measured yet - **Documentation Coverage:** 100% (all issues documented) -- **Test Coverage:** 100% (all modes tested) +- **Behavioral Test Coverage:** Configuration, read-only modes, path safety, and all workspace profiles ### Issues - **Critical Issues Fixed:** 5/5 (100%) @@ -279,15 +276,15 @@ HTTP allowed only on documented allow-list with strong justification. ## RECOMMENDATION FOR NEXT PHASE -**Ready to begin Phase 9 (Implementation)?** +**Ready for the remaining hardening and deployment work?** -The design and specification are complete. All dependencies, requirements, and architectural decisions have been documented. You can now: +The design is complete and the first five implementation weeks are validated. The remaining work is focused on hardening and production rollout: -1. **Allocate development resources** — ~40 hours for Phase 9 implementation -2. **Review specifications** — Ensure all phases meet requirements -3. **Begin script refactoring** — Start with core infrastructure (Week 1) -4. **Follow the roadmap** — 5-week implementation timeline -5. **Deploy and validate** — Test on clean VMs, iterate, deploy to production +1. **Complete remaining mode semantics** — Repair, Inventory, and automatic resume skipping +2. **Harden download trust** — Populate trusted hashes and add malware scanning +3. **Split modules** — Extract configuration, download, Windows, workspace, and reporting modules +4. **Run elevated Apply tests** — Validate WinGet, portable downloads, drivers, and restart behavior +5. **Package and deploy** — Test on clean workstations before production rollout All 9 phase documents are ready to guide development. The specifications are detailed enough for a developer to implement without extensive back-and-forth. @@ -307,17 +304,15 @@ For questions about: --- -**Project Status:** ✅ **DESIGN & SPECIFICATION COMPLETE** +**Project Status:** ✅ **DESIGN COMPLETE; WEEKS 1-5 IMPLEMENTED AND VALIDATED** -All 9 phases are fully documented and ready for implementation. +All 9 phase documents are complete. The implementation now includes profile-aware applications, secure download gates, Windows configuration, checkpoint persistence, workspace initialization, automated tests, and operator documentation. -The automotive workstation setup system has been comprehensively audited, architecturally redesigned, and thoroughly specified for production-grade deployment. - -**Ready to build! 🚀** +The remaining work is production hardening and elevated workstation validation. --- *Last Updated: 2026-09-09* -*Project Duration: ~172 hours of design and specification work* -*Implementation Timeline: ~4-5 weeks* +*Project Duration: Design plus five implementation weeks* +*Last validation: 10 Pester tests passed* diff --git a/Config/windows-settings.json b/Config/windows-settings.json new file mode 100644 index 0000000..b4a9c99 --- /dev/null +++ b/Config/windows-settings.json @@ -0,0 +1,38 @@ +{ + "SchemaVersion": "1.0", + "Profiles": { + "DailyTech-Tuning": { + "PowerPlan": "High performance", + "PowerPlanAlias": "SCHEME_MIN", + "Hibernation": "Disabled", + "UsbSelectiveSuspend": "Disabled", + "MonitorTimeoutACMinutes": 0, + "DiskTimeoutACMinutes": 0, + "StandbyTimeoutACMinutes": 0, + "ShowFileExtensions": true, + "LongPathsEnabled": true + }, + "ODIS-XENTRY": { + "PowerPlan": "Balanced", + "PowerPlanAlias": "SCHEME_BALANCED", + "Hibernation": "Disabled", + "UsbSelectiveSuspend": "Disabled", + "MonitorTimeoutACMinutes": 30, + "DiskTimeoutACMinutes": 0, + "StandbyTimeoutACMinutes": 60, + "ShowFileExtensions": true, + "LongPathsEnabled": true + }, + "PIWIS-ISTA": { + "PowerPlan": "Balanced", + "PowerPlanAlias": "SCHEME_BALANCED", + "Hibernation": "Disabled", + "UsbSelectiveSuspend": "Disabled", + "MonitorTimeoutACMinutes": 30, + "DiskTimeoutACMinutes": 0, + "StandbyTimeoutACMinutes": 60, + "ShowFileExtensions": true, + "LongPathsEnabled": true + } + } +} diff --git a/Config/workspace-templates.json b/Config/workspace-templates.json new file mode 100644 index 0000000..1958070 --- /dev/null +++ b/Config/workspace-templates.json @@ -0,0 +1,56 @@ +{ + "SchemaVersion": "1.0", + "Templates": { + "DailyTech-Tuning": { + "LocalBase": "Projects\\Tuning", + "SharedBase": "", + "Folders": [ + "Originals", + "RepeatedReads", + "WorkingCopies\\v1", + "Checksums", + "BinaryDifferences", + "Backups", + "Reports", + "Logs", + "CAN_Data\\DBC_Files", + "Scripts", + "Documentation", + "Photos", + "FinalDelivery", + "ArchivedProjects" + ], + "OperationType": "Tuning" + }, + "ODIS-XENTRY": { + "LocalBase": "Projects\\Diagnostics\\ODIS", + "SharedBase": "", + "Folders": [ + "SessionLogs", + "CodingBackups", + "FlashFiles", + "Screenshots", + "DiagnosticReports", + "CAN_Data", + "Configuration", + "Documentation" + ], + "OperationType": "Diagnostics" + }, + "PIWIS-ISTA": { + "LocalBase": "Projects\\Diagnostics\\PIWIS", + "SharedBase": "", + "Folders": [ + "PIWIS_Sessions", + "PSdZData", + "CodingBackups", + "ISTA_Logs", + "Screenshots", + "MeasuringPlans", + "Reports", + "Documentation" + ], + "OperationType": "Diagnostics" + } + } +} diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..687304f --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,38 @@ +# Troubleshooting + +## Pester is not installed + +Install Pester for the current user, then rerun the test runner: + +```powershell +Install-Module Pester -Scope CurrentUser -Force +.\Tests\\Run-AllTests.ps1 +``` + +## Script reports that administrator privileges are required + +`Apply`, `Repair`, `Restore`, `UpdatePortable`, and Windows configuration changes require an elevated Windows PowerShell window. Use `Plan`, `Audit`, `HealthCheck`, or `CreateWorkspace` when elevation is not available. + +## A shared drive is unavailable + +The setup expects `S:` for shared automotive data and `P:` for shared portable applications. `HealthCheck` reports their availability and labels. Portable applications fall back to the local `C:\Automotive\PortableApps` path when `P:` is unavailable, but shared profile data remains unavailable until the drive is mounted. + +## A download is blocked as insecure + +Downloads must use HTTPS. HTTP is rejected unless the caller explicitly marks a documented vendor exception. The current exception is limited to the Xhorse assets whose vendor CDN does not provide HTTPS. Verify the vendor network/VPN path and confirm the URL before retrying. + +## A portable download fails hash or signature validation + +Do not bypass the failure. Preserve the downloaded file, compare its SHA256 value with the trusted vendor release information, and inspect the Authenticode status. Remove the file only after the incident has been recorded. Update `Config\\portable-apps.json` only with a verified expected hash. + +## Resume after restart + +Check `C:\Automotive\Config\\.workstation-setup-state.json` or the configured local root for the last checkpoint. Resume with the same profile and `-ResumeFromCheckpoint`. Completed work is revalidated because installers and vendor packages can change between runs. + +## Workspace creation fails + +`CreateWorkspace` requires `-JobId`, `-Manufacturer`, and `-Model`. Job IDs, manufacturers, and models cannot contain path separators or Windows filename characters. Use a simple value such as `VW001`, `Volkswagen`, and `Golf`. + +## OEM or communication software installation + +The setup does not silently install licensed OEM suites. Use the vendor installer, complete licensing and device registration on the current Windows installation, and verify J2534/USB drivers separately. Keep other isolated Windows installations without drive letters while working in the current profile. diff --git a/Tests/Configuration.Tests.ps1 b/Tests/Configuration.Tests.ps1 new file mode 100644 index 0000000..f18f44e --- /dev/null +++ b/Tests/Configuration.Tests.ps1 @@ -0,0 +1,37 @@ +Describe 'Configuration integrity' { + BeforeAll { + $script:repositoryRoot = Split-Path $PSScriptRoot -Parent + $script:configRoot = Join-Path $script:repositoryRoot 'Config' + } + + It 'parses every JSON configuration file' { + $files = @(Get-ChildItem -LiteralPath $script:configRoot -Filter '*.json' -File) + $files.Count | Should -BeGreaterThan 0 + foreach ($file in $files) { + { Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json -ErrorAction Stop } | Should -Not -Throw + } + } + + It 'defines all supported workstation profiles' { + $profiles = Get-Content (Join-Path $script:configRoot 'workstation-profiles.json') -Raw | ConvertFrom-Json + @($profiles.Profiles.Name) | Should -Contain 'DailyTech & Tuning' + @($profiles.Profiles.Name) | Should -Contain 'ODIS & XENTRY' + @($profiles.Profiles.Name) | Should -Contain 'PIWIS & ISTA' + } + + It 'defines Windows settings and workspace templates for every profile folder' { + $profiles = Get-Content (Join-Path $script:configRoot 'workstation-profiles.json') -Raw | ConvertFrom-Json + $windows = Get-Content (Join-Path $script:configRoot 'windows-settings.json') -Raw | ConvertFrom-Json + $templates = Get-Content (Join-Path $script:configRoot 'workspace-templates.json') -Raw | ConvertFrom-Json + foreach ($profile in $profiles.Profiles) { + $windows.Profiles.PSObject.Properties.Name | Should -Contain $profile.Folder + $templates.Templates.PSObject.Properties.Name | Should -Contain $profile.Folder + } + } + + It 'contains no duplicate application catalog IDs' { + $catalog = Get-Content (Join-Path $script:configRoot 'app-catalog.json') -Raw | ConvertFrom-Json + $ids = @($catalog.Packages.Id) + @($ids | Select-Object -Unique).Count | Should -Be $ids.Count + } +} diff --git a/Tests/ExecutionModes.Tests.ps1 b/Tests/ExecutionModes.Tests.ps1 new file mode 100644 index 0000000..0b9499c --- /dev/null +++ b/Tests/ExecutionModes.Tests.ps1 @@ -0,0 +1,40 @@ +Describe 'Execution modes' { + BeforeAll { + $script:repositoryRoot = Split-Path $PSScriptRoot -Parent + $script:scriptPath = Join-Path $script:repositoryRoot 'Automotive-Workstation-Setup.ps1' + $script:newTestRoot = { Join-Path ([IO.Path]::GetTempPath()) ('automotive-pester-' + [guid]::NewGuid().ToString('N')) } + } + + It 'generates a plan without requiring administrator privileges' { + $root = & $script:newTestRoot + try { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script:scriptPath -Mode Plan ` + -LocalRoot $root -SharedDataRoot (Join-Path $root 'SharedData') ` + -SharedPortableRoot (Join-Path $root 'PortableApps') + $LASTEXITCODE | Should -Be 0 + Get-ChildItem -LiteralPath (Join-Path $root 'Logs') -Filter '*ApplicationPlan*.csv' | Should -Not -BeNullOrEmpty + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'runs HealthCheck without entering the administrator-gated apply path' { + $root = & $script:newTestRoot + try { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script:scriptPath -Mode HealthCheck ` + -LocalRoot $root -SharedDataRoot (Join-Path $root 'SharedData') ` + -SharedPortableRoot (Join-Path $root 'PortableApps') -SkipDownloads -SkipWinget -SkipShortcuts + $LASTEXITCODE | Should -Be 0 + Get-ChildItem -LiteralPath $root -Recurse -Filter 'HealthReport_*.json' | Should -Not -BeNullOrEmpty + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'does not contain the old hard-coded installer root' { + $content = Get-Content -LiteralPath $script:scriptPath -Raw + $content | Should -Not -Match '\$ProfileInstallerRoot\s*=\s*["'']P:\\Install' + } +} diff --git a/Tests/Run-AllTests.ps1 b/Tests/Run-AllTests.ps1 new file mode 100644 index 0000000..fde0cd3 --- /dev/null +++ b/Tests/Run-AllTests.ps1 @@ -0,0 +1,16 @@ +[CmdletBinding()] +param( + [string]$TestPath +) + +$ErrorActionPreference = 'Stop' +if ([string]::IsNullOrWhiteSpace($TestPath)) { + $TestPath = Split-Path -Parent $MyInvocation.MyCommand.Path +} +if (-not (Get-Module -ListAvailable -Name Pester)) { + throw 'Pester is not installed. Install it with: Install-Module Pester -Scope CurrentUser -Force' +} + +Import-Module Pester -MinimumVersion 5.0 -ErrorAction Stop +$result = Invoke-Pester -Path (Join-Path $TestPath '*.Tests.ps1') -Output Detailed -PassThru +if ($result.FailedCount -gt 0) { exit 1 } diff --git a/Tests/Workspace.Tests.ps1 b/Tests/Workspace.Tests.ps1 new file mode 100644 index 0000000..43cf35e --- /dev/null +++ b/Tests/Workspace.Tests.ps1 @@ -0,0 +1,54 @@ +Describe 'CreateWorkspace mode' { + BeforeAll { + $script:repositoryRoot = Split-Path $PSScriptRoot -Parent + $script:scriptPath = Join-Path $script:repositoryRoot 'Automotive-Workstation-Setup.ps1' + $script:newTestRoot = { Join-Path ([IO.Path]::GetTempPath()) ('automotive-workspace-pester-' + [guid]::NewGuid().ToString('N')) } + } + + It 'creates the DailyTech template and metadata' { + $root = & $script:newTestRoot + try { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script:scriptPath -Mode CreateWorkspace ` + -WorkstationProfile 'DailyTech & Tuning' -LocalRoot $root ` + -SharedDataRoot (Join-Path $root 'SharedData') -SharedPortableRoot (Join-Path $root 'PortableApps') ` + -JobId 'TEST001' -Manufacturer 'Volkswagen' -Model 'Golf' -Module ECU + $LASTEXITCODE | Should -Be 0 + $project = Get-ChildItem (Join-Path $root 'Projects\Tuning') -Directory | Select-Object -First 1 + Test-Path (Join-Path $project.FullName 'ProjectMetadata.json') | Should -BeTrue + Test-Path (Join-Path $project.FullName 'Originals') | Should -BeTrue + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'rejects path traversal in JobId' { + $root = & $script:newTestRoot + try { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script:scriptPath -Mode CreateWorkspace ` + -LocalRoot $root -SharedDataRoot (Join-Path $root 'SharedData') -SharedPortableRoot (Join-Path $root 'PortableApps') ` + -JobId '..\escape' -Manufacturer 'BMW' -Model 'Test' + $LASTEXITCODE | Should -Not -Be 0 + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'creates profile-specific diagnostic roots' { + foreach ($profile in @('ODIS & XENTRY', 'PIWIS & ISTA')) { + $root = & $script:newTestRoot + try { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script:scriptPath -Mode CreateWorkspace ` + -WorkstationProfile $profile -LocalRoot $root ` + -SharedDataRoot (Join-Path $root 'SharedData') -SharedPortableRoot (Join-Path $root 'PortableApps') ` + -JobId 'TEST002' -Manufacturer 'BMW' -Model 'Test Model' -Module ECU + $LASTEXITCODE | Should -Be 0 + Get-ChildItem (Join-Path $root 'Projects\Diagnostics') -Directory -Recurse | Where-Object Name -like 'TEST002_*' | Should -Not -BeNullOrEmpty + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } + } + } +} diff --git a/USER-GUIDE.md b/USER-GUIDE.md new file mode 100644 index 0000000..95819d9 --- /dev/null +++ b/USER-GUIDE.md @@ -0,0 +1,61 @@ +# Automotive Workstation Setup User Guide + +## Modes + +Run the script from an elevated Windows PowerShell session for installation modes. Read-only modes do not require elevation. + +```powershell +# Preview changes +.\Automotive-Workstation-Setup.ps1 -Mode Plan -WorkstationProfile 'DailyTech & Tuning' + +# Inspect the current workstation +.\Automotive-Workstation-Setup.ps1 -Mode Audit -WorkstationProfile 'ODIS & XENTRY' + +# Apply the selected profile +.\Automotive-Workstation-Setup.ps1 -Mode Apply -WorkstationProfile 'PIWIS & ISTA' + +# Create a project workspace +.\Automotive-Workstation-Setup.ps1 -Mode CreateWorkspace ` + -WorkstationProfile 'DailyTech & Tuning' ` + -JobId 'VW001' -Manufacturer 'Volkswagen' -Model 'Golf' ` + -ModelYear 2015 -Module ECU -Technician 'Technician Name' +``` + +`-HealthCheck`, `-BackupConfiguration`, and `-RestoreConfiguration` remain supported for compatibility with older commands. + +## Profiles + +- `DailyTech & Tuning`: tuning, binary analysis, reverse engineering, and CAN work. +- `ODIS & XENTRY`: VAG and Mercedes diagnostic sessions. +- `PIWIS & ISTA`: Porsche and BMW diagnostic sessions. + +The profile determines application selection, Windows power policy, and workspace structure. + +## Reports and State + +Reports are written under the configured local root: + +- `Logs`: setup logs, CSV summaries, plans, and workspace results. +- `Config`: local profile metadata, Windows state, and checkpoint state. +- `Manifests`: portable application hashes and inventories when the shared portable root is available. + +The checkpoint file is `Config\\.workstation-setup-state.json`. Resume a prior apply run with: + +```powershell +.\Automotive-Workstation-Setup.ps1 -Mode Apply ` + -WorkstationProfile 'DailyTech & Tuning' -ResumeFromCheckpoint +``` + +## Workspace Data Handling + +Always preserve original ECU, EEPROM, and coding reads in the `Originals` or profile-equivalent folder. Calculate hashes before editing. Keep modified files in `WorkingCopies`, `CodingBackups`, or the matching profile folder. Do not flash or code a vehicle without authorization, verified backups, stable power, and a vendor-supported procedure. + +## Testing + +Run the Pester suite from the repository root: + +```powershell +.\Tests\\Run-AllTests.ps1 +``` + +The suite is designed to use temporary roots and does not install applications or modify Windows settings.