Private/Invoke-Win32ToolkitManualWizard.ps1

function Invoke-Win32ToolkitManualWizard {
    <#
    .SYNOPSIS
        Guided manual (non-winget) packaging wizard (Spectre): metadata → installer → mode →
        template → options → summary → confirm → New-Win32ToolkitManualApp. Advanced apps get a
        two-step finish (edit the Install region, then Complete-…). See knowledge-base/designs/tui.md.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$BasePath)

    Clear-Host
    Write-SpectreRule -Title 'Package a manual app (not in winget)' -Color Blue

    # Mode
    $modeSel = Read-SpectreSelection -Message 'How does this app install?' -Choices @(
        [pscustomobject]@{ Key = 'easy';     Label = 'Easy — a silent switch installs it (e.g. /S, /qn), or it is an MSI' }
        [pscustomobject]@{ Key = 'advanced'; Label = 'Advanced — I will write the install logic myself' }
    ) -ChoiceLabelProperty 'Label' -Color Blue
    $advanced = ($modeSel.Key -eq 'advanced')

    # Installer
    $source = Read-Win32ToolkitValidatedText -Message 'Full path to the installer file (or a folder of files)' `
        -Validator { Test-Path -LiteralPath ($args[0].Trim().Trim('"')) } `
        -ErrorMessage 'Not found — enter a valid path to the installer file or folder.'
    $source = $source.Trim().Trim('"')

    # Metadata
    $name    = Read-Win32ToolkitValidatedText -Message 'Application name' -Validator { -not [string]::IsNullOrWhiteSpace($args[0]) } -ErrorMessage 'Name is required.'
    $version = Read-Win32ToolkitValidatedText -Message 'Version (e.g. 1.2.3)' -Validator { -not [string]::IsNullOrWhiteSpace($args[0]) } -ErrorMessage 'Version is required.'
    $arch    = Read-SpectreSelection -Message 'Target architecture' -Choices @('x64', 'x86', 'arm64') -Color Blue

    $publisher = Read-Win32ToolkitValidatedText -Message 'Publisher (optional)' -AllowEmpty
    $descr     = Read-Win32ToolkitValidatedText -Message 'Description (optional)' -AllowEmpty
    $infoUrl   = Read-Win32ToolkitValidatedText -Message 'Information URL (optional)' -AllowEmpty
    $iconPath  = Read-Win32ToolkitValidatedText -Message 'Path to an icon .png/.ico (optional)' -AllowEmpty `
        -Validator { [string]::IsNullOrWhiteSpace($args[0]) -or (Test-Path -LiteralPath ($args[0].Trim().Trim('"')) -PathType Leaf) } `
        -ErrorMessage 'Icon file not found.'
    if ($iconPath) { $iconPath = $iconPath.Trim().Trim('"') }

    # Easy: silent args
    $silentArgs = ''
    if (-not $advanced) {
        $silentArgs = Read-Win32ToolkitValidatedText -Message 'Silent-install switch(es) (e.g. /S, or /qn /norestart; blank if MSI)' -AllowEmpty
    }

    # Template
    $template = Get-Win32ToolkitTemplateChoice -BasePath $BasePath
    if ([string]::IsNullOrWhiteSpace($template)) { return }

    # Dependencies — apps Intune must install BEFORE this one (e.g. a VC++ redistributable). Also staged
    # into the test/capture guest so the run installs them first, like Intune does on a device.
    $deps = @()
    if (Read-SpectreConfirm -Message 'Does this app need another app installed FIRST (e.g. a VC++ redistributable)?' -DefaultAnswer 'n') {
        $deps = @(Show-Win32ToolkitDependencyPicker -BasePath $BasePath)
    }

    # After-build options. The documentation capture ALWAYS runs, and both it and the optional test follow
    # the CONFIGURED backend — name the real backend rather than hard-coding "Windows Sandbox".
    $bi = Get-Win32ToolkitBackendInfo
    if ($bi.FellBack) {
        Write-SpectreHost "[yellow]Hyper-V is selected but NOT ready — falling back to Windows Sandbox:[/] $(Get-SpectreEscapedText -Text ($bi.Reasons -join '; '))"
    }
    $testLabel = "Run an install/uninstall test in $($bi.Label)"
    $actions = @(Read-SpectreMultiSelection -Message 'After building, also… (space to toggle, enter to accept)' -Choices @(
            $testLabel
            'Package to .intunewin'
            'Publish to Intune'
        ) -Color Blue -AllowEmpty)
    $doTest    = $actions -contains $testLabel
    $doPackage = $actions -contains 'Package to .intunewin'
    $doPublish = $actions -contains 'Publish to Intune'

    # Summary
    $after = @()
    if ($doTest)    { $after += 'install/uninstall test' }
    if ($doPackage) { $after += 'package (.intunewin)' }
    if ($doPublish) { $after += 'PUBLISH to Intune' }
    $afterStr = if ($after) { $after -join ', ' } else { 'nothing (just build the project)' }
    $modeStr  = if ($advanced) { 'Advanced (you write the install logic)' } elseif ($silentArgs) { "Easy (silent: $silentArgs)" } else { 'Easy (MSI / Zero-Config)' }
    $summary = @(
        "Application : $name (v$version)"
        "Architecture : $arch"
        "Installer : $source"
        "Publisher : $(if ($publisher) { $publisher } else { '(none)' })"
        "Template : $template"
        "Mode : $modeStr"
        "Base folder : $BasePath"
        "Test/capture : $($bi.Label)"
        "Depends on : $(if ($deps) { $deps -join ', ' } else { '(nothing)' })"
        "After build : $afterStr"
    ) -join "`n"
    Format-SpectrePanel -Data (Get-SpectreEscapedText -Text $summary) -Header 'Review' -Border Rounded -Color Blue

    # Confirm (+ publish gate)
    if (-not (Read-SpectreConfirm -Message 'Continue?' -DefaultAnswer 'n')) {
        Write-SpectreHost '[yellow]Cancelled.[/]'; Read-SpectrePause -Message 'Press any key to return' -AnyKey | Out-Null; return
    }
    if ($doPublish -and -not (Read-SpectreConfirm -Message 'Publishing UPLOADS to your Intune tenant (you will sign in). Continue?' -DefaultAnswer 'n')) {
        $doPublish = $false
        Write-SpectreHost '[yellow]Publish skipped.[/]'
    }

    # Common params for New-Win32ToolkitManualApp
    $p = @{ Name = $name; Version = $version; Architecture = $arch; SourcePath = $source; TemplateName = $template; BasePath = $BasePath; Force = $true }
    if ($publisher) { $p.Publisher = $publisher }
    if ($descr)     { $p.Description = $descr }
    if ($infoUrl)   { $p.InformationUrl = $infoUrl }
    if ($iconPath)  { $p.IconPath = $iconPath }
    if ($deps)      { $p.DependsOn = $deps }   # declared into AppConfig + staged into the test/capture guest

    if ($advanced) {
        # Scaffold only, then hand off to the two-step finish screen.
        Clear-Host
        Write-SpectreRule -Title "Scaffolding $(Get-SpectreEscapedText -Text $name)…" -Color Blue
        $p.Advanced = $true
        try { New-Win32ToolkitManualApp @p | Out-Null }
        catch {
            Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Scaffold failed' -Border Rounded -Color Red
            Read-SpectrePause -Message 'Press any key to return' -AnyKey | Out-Null
            return
        }
        $paths       = Get-Win32ToolkitPaths -BasePath $BasePath
        $templateSeg = Sanitize-ProjectName -Name $template
        if ([string]::IsNullOrWhiteSpace($templateSeg)) { $templateSeg = 'Default' }
        $projName    = '{0}_{1}_{2}' -f (Sanitize-ProjectName -Name $name), (Sanitize-ProjectName -Name $arch), (Sanitize-ProjectName -Name $version)
        $projPath    = Join-Path (Join-Path $paths.Projects $templateSeg) $projName
        Show-Win32ToolkitAdvancedFinish -ProjectPath $projPath -DoTest:$doTest -DoPackage:$doPackage -DoPublish:$doPublish
    }
    else {
        # Easy — build end to end inline.
        $p.SilentArgs = $silentArgs
        $p.Continue = $true
        if ($doTest)    { $p.RunTest = 'InstallUninstall' }
        if ($doPackage) { $p.PackageIntune = $true }
        if ($doPublish) { $p.PublishIntune = $true }
        Clear-Host
        Write-SpectreRule -Title "Building $(Get-SpectreEscapedText -Text $name)…" -Color Blue
        try {
            New-Win32ToolkitManualApp @p | Out-Null
            Format-SpectrePanel -Data "Finished [green]$(Get-SpectreEscapedText -Text $name)[/].`nUse [blue]Browse projects[/] to find it; review the messages above for details." -Header 'Done' -Border Rounded -Color Green
        }
        catch {
            Format-SpectrePanel -Data "[red]$(Get-SpectreEscapedText -Text $_.Exception.Message)[/]" -Header 'Something went wrong' -Border Rounded -Color Red
        }
        Read-SpectrePause -Message 'Press any key to return to the menu' -AnyKey | Out-Null
    }
}