Private/Show-Win32ToolkitProjectActions.ps1
|
function Show-Win32ToolkitProjectActions { <# .SYNOPSIS "Work with an existing project" screen (Spectre): pick a project, then test / finalize / package / publish (gated) / open. Thin front-end over the existing commands. See knowledge-base/designs/tui.md. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$BasePath) while ($true) { Clear-Host Write-SpectreRule -Title 'Work with an existing project' -Color Blue $projects = @(Get-PSADTProjects -BasePath $BasePath) if ($projects.Count -eq 0) { Format-SpectrePanel -Data 'No projects yet — create one with [blue]Package an app[/].' -Header 'No projects' -Border Rounded -Color Yellow Read-SpectrePause -Message 'Press any key to return' -AnyKey | Out-Null return } # Select a project (string label + lookup) $map = [ordered]@{} $labels = foreach ($p in ($projects | Sort-Object Template, Name)) { $label = Get-SpectreEscapedText -Text ('{0} / {1}' -f $p.Template, $p.Name) $map[$label] = $p $label } $chosen = Read-SpectreSelection -Message 'Select a project (type to filter)' -Choices @($labels) -Color Blue -EnableSearch -PageSize 15 $project = $map[$chosen] if (-not $project) { return } $reselect = $false while (-not $reselect) { Clear-Host Write-SpectreRule -Title (Get-SpectreEscapedText -Text ('{0} / {1}' -f $project.Template, $project.Name)) -Color Blue $depNow = @(Get-Win32ToolkitDependencies -ProjectPath $project.Path) $act = Read-SpectreSelection -Message 'What would you like to do?' -Choices @( [pscustomobject]@{ Key = 'test'; Label = 'Run a test (Windows Sandbox or Hyper-V VM)' } [pscustomobject]@{ Key = 'deps'; Label = "Dependencies — apps installed BEFORE this one ($(if ($depNow.Count) { "$($depNow.Count) declared" } else { 'none' }))" } [pscustomobject]@{ Key = 'finish'; Label = "Finalize / refresh ($((Get-Win32ToolkitBackendInfo).Label) capture -> auto uninstall)" } [pscustomobject]@{ Key = 'package'; Label = 'Package to .intunewin' } [pscustomobject]@{ Key = 'publish'; Label = 'Publish to Intune' } [pscustomobject]@{ Key = 'doc'; Label = 'Generate customer documentation (Documentation.md)' } [pscustomobject]@{ Key = 'open'; Label = 'Open the project folder' } [pscustomobject]@{ Key = 'another'; Label = 'Pick another project' } [pscustomobject]@{ Key = 'back'; Label = 'Back to the main menu' } ) -ChoiceLabelProperty 'Label' -Color Blue -PageSize 10 switch ($act.Key) { 'deps' { Clear-Host; Write-SpectreRule -Title 'Dependencies' -Color Blue Write-SpectreHost '[grey]Intune installs these BEFORE this app. They are also installed first in the test/capture run, so the app is never tested without its runtime.[/]' $current = @($depNow | ForEach-Object { "$($_.Source):$($_.Ref)" }) $chosen = @(Show-Win32ToolkitDependencyPicker -BasePath $BasePath -Existing $current) try { # The picker returns the AUTHORITATIVE list, so -Clear first and re-declare it. if ($chosen.Count -eq 0) { $null = Set-Win32ToolkitAppDependency -ProjectPath $project.Path -Clear } else { $null = Set-Win32ToolkitAppDependency -ProjectPath $project.Path -DependsOn $chosen -Clear } $shown = if ($chosen.Count) { $chosen -join ', ' } else { '(none)' } Format-SpectrePanel -Data "Dependencies saved: $(Get-SpectreEscapedText -Text $shown)`n`nRe-run Finalize/refresh so the capture reflects them, and re-publish to attach the Intune relationships." -Header 'Done' -Border Rounded -Color Green | Out-SpectreHost } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red | Out-SpectreHost } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } 'test' { $backend = Read-SpectreSelection -Message 'Which test backend?' -Choices @( [pscustomobject]@{ Key = 'Sandbox'; Label = 'Windows Sandbox' } [pscustomobject]@{ Key = 'HyperV'; Label = 'Hyper-V VM (fast — needs a provisioned test VM; see Settings)' } ) -ChoiceLabelProperty 'Label' -Color Blue # Both scenarios run on both backends now (Update-on-HyperV drives the same # PreBaseline -> install-old -> PreUpdate -> pause -> update -> PostUpdate sequence # as the Sandbox LogonCommand, with a HOST pause instead of the in-guest countdown). $scenChoices = @( [pscustomobject]@{ Key = 'InstallUninstall'; Label = 'Install then uninstall (any app)' } [pscustomobject]@{ Key = 'Update'; Label = 'Update from an older version (winget download or a local packaged baseline)' } ) $sc = Read-SpectreSelection -Message 'Test scenario' -Choices $scenChoices -ChoiceLabelProperty 'Label' -Color Blue $testSplat = @{ ProjectPath = $project.Path; Scenario = $sc.Key; Backend = $backend.Key } if ($backend.Key -eq 'HyperV') { $pauseWhat = if ($sc.Key -eq 'Update') { 'verify the OLD install, then update' } else { 'test the app, then uninstall' } $mode = Read-SpectreSelection -Message 'Hyper-V run mode' -Choices @( [pscustomobject]@{ Key = 'interactive'; Label = "Interactive — watch the PSADT GUI in the VM console, $pauseWhat" } [pscustomobject]@{ Key = 'unattended'; Label = 'Silent — run every phase back-to-back (no GUI, no pause)' } ) -ChoiceLabelProperty 'Label' -Color Blue if ($mode.Key -eq 'unattended') { $testSplat['Unattended'] = $true } } $abort = $false if ($sc.Key -eq 'Update') { # Old-version baseline source: download an older winget version, OR install a LOCAL # packaged project as the baseline (the friendly 'project:' way — same as # -BaselineProject on the cmdlet). A manual (non-winget) app has no winget baseline, # so a local package is the only option there (and today's default winget path would # hard-error mid-run for it). $wingetId = Get-WingetIdFromProject -FilesPath (Join-Path $project.Path 'Files') $isWinget = -not [string]::IsNullOrWhiteSpace($wingetId) $baselineCandidates = @(Get-PSADTProjects -BasePath $BasePath | Where-Object { $_.Path -ne $project.Path }) $useLocal = $false if ($isWinget) { $srcPick = Read-SpectreSelection -Message 'Old-version baseline source' -Choices @( [pscustomobject]@{ Key = 'winget'; Label = 'Download an older version from winget' } [pscustomobject]@{ Key = 'project'; Label = "Use a local packaged project ($($baselineCandidates.Count) available)" } ) -ChoiceLabelProperty 'Label' -Color Blue $useLocal = ($srcPick.Key -eq 'project') if ($useLocal -and $baselineCandidates.Count -eq 0) { Format-SpectrePanel -Data 'No other packaged projects to use as a baseline yet — falling back to the winget download.' -Header 'No local baseline' -Border Rounded -Color Yellow | Out-SpectreHost $useLocal = $false } } elseif ($baselineCandidates.Count -eq 0) { # Manual app AND nothing to use as a baseline — the update test cannot run. Format-SpectrePanel -Data 'This is a manual (non-winget) app, so the update test needs a LOCAL packaged project as the old-version baseline — and none are available yet. Package an older version first, then re-run.' -Header 'No baseline available' -Border Rounded -Color Yellow | Out-SpectreHost Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null $abort = $true } else { Write-SpectreHost '[grey]Manual (non-winget) app — choose a local packaged project as the old-version baseline.[/]' $useLocal = $true } if (-not $abort -and $useLocal) { $bmap = [ordered]@{} $blabels = foreach ($bp in ($baselineCandidates | Sort-Object Template, Name)) { $bver = $null try { $bver = (Get-Win32ToolkitAppConfig -ProjectPath $bp.Path).App.Version } catch { $bver = $null } $label = Get-SpectreEscapedText -Text ('{0} / {1}{2}' -f $bp.Template, $bp.Name, $(if ($bver) { " (v$bver)" } else { '' })) $bmap[$label] = $bp $label } $bchosen = Read-SpectreSelection -Message 'Baseline project (the OLDER version to upgrade FROM)' -Choices @($blabels) -Color Blue -EnableSearch -PageSize 15 $bproj = $bmap[$bchosen] if ($bproj) { $testSplat['BaselineProjectPath'] = $bproj.Path } else { $abort = $true } } if (-not $abort) { if (-not (Read-SpectreConfirm -Message 'Also verify the update-app requirement rule during the test? (recommended)' -DefaultAnswer 'y')) { $testSplat['SkipRequirementCheck'] = $true } } } if (-not $abort) { Clear-Host; Write-SpectreRule -Title "Running the $($backend.Key) test…" -Color Blue try { $verdict = Test-Win32ToolkitProject @testSplat if ($sc.Key -eq 'Update') { if ($verdict -eq $true) { Format-SpectrePanel -Data 'All in-sandbox assertions passed.' -Header 'UPDATE TEST PASSED' -Border Rounded -Color Green } elseif ($verdict -eq $false) { Format-SpectrePanel -Data 'One or more assertions FAILED - see Sandbox\Logs\UpdateAssertions.log in the project folder.' -Header 'UPDATE TEST FAILED' -Border Rounded -Color Red } else { Format-SpectrePanel -Data 'No conclusive assertion results (sandbox closed early, timeout, or everything skipped).' -Header 'Inconclusive' -Border Rounded -Color Yellow } } } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } } 'finish' { Clear-Host; Write-SpectreRule -Title 'Finalizing…' -Color Blue try { Complete-Win32ToolkitManualApp -ProjectPath $project.Path } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } 'package' { Clear-Host; Write-SpectreRule -Title 'Packaging…' -Color Blue try { Export-Win32ToolkitIntuneWin -ProjectPath $project.Path -NoPublishPrompt Format-SpectrePanel -Data 'Package created. See the messages above for the .intunewin path.' -Header 'Done' -Border Rounded -Color Green } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } 'publish' { $cfg = Get-Win32ToolkitAppConfig -ProjectPath $project.Path $app = if ($cfg.PSObject.Properties.Name -contains 'App') { $cfg.App } else { $null } $summary = @( "Application : $(if ($app.Name) { $app.Name } else { $project.Name })" "Version : $($app.Version)" "Publisher : $($app.Vendor)" ) -join "`n" Format-SpectrePanel -Data (Get-SpectreEscapedText -Text $summary) -Header 'About to PUBLISH to Intune' -Border Rounded -Color Yellow $which = Read-SpectreSelection -Message 'Publish which app?' -Choices @( [pscustomobject]@{ Key = 'install'; Label = 'Install app — normal deployment (assign to target devices)' } [pscustomobject]@{ Key = 'both'; Label = 'Install app + Update app (2nd app, only where already installed)' } [pscustomobject]@{ Key = 'update'; Label = 'Update app only — applies only where the app is already installed' } [pscustomobject]@{ Key = 'cancel'; Label = 'Cancel' } ) -ChoiceLabelProperty 'Label' -Color Blue -PageSize 10 if ($which.Key -ne 'cancel') { Write-SpectreHost "[yellow]You will sign in to Microsoft Graph — the target tenant is shown during sign-in.[/]" if (Read-SpectreConfirm -Message 'Package and upload now?' -DefaultAnswer 'n') { Clear-Host; Write-SpectreRule -Title 'Publishing to Intune…' -Color Blue try { $splat = @{ ProjectPath = $project.Path } switch ($which.Key) { 'install' { $splat['PublishIntune'] = $true } 'update' { $splat['PublishUpdate'] = $true } 'both' { $splat['PublishIntune'] = $true; $splat['PublishUpdate'] = $true } } Export-Win32ToolkitIntuneWin @splat Format-SpectrePanel -Data 'Published. See the messages above for the app ID(s) and portal link.' -Header 'Done' -Border Rounded -Color Green } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } } } 'doc' { Clear-Host; Write-SpectreRule -Title 'Generate customer documentation' -Color Blue Write-SpectreHost '[grey]A clean one-page Documentation.md summarising the app, how it deploys, and the tests that were run — safe to hand to a customer.[/]' $inclIds = Read-SpectreConfirm -Message 'Include the Intune app id + portal link? (only if the customer owns the tenant)' -DefaultAnswer 'n' try { $out = Export-Win32ToolkitDocumentation -ProjectPath $project.Path -IncludeIntuneIds:$inclIds Format-SpectrePanel -Data "Documentation written to:`n$(Get-SpectreEscapedText -Text $out)" -Header 'Done' -Border Rounded -Color Green | Out-SpectreHost } catch { Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Error' -Border Rounded -Color Red | Out-SpectreHost } Read-SpectrePause -Message 'Press any key to continue' -AnyKey | Out-Null } 'open' { try { Invoke-Item -LiteralPath $project.Path } catch { } } 'another' { $reselect = $true } 'back' { return } } } } } |