Private/New-OrgTemplate.ps1

function New-OrgTemplate {
    [CmdletBinding()]
    param(
        [PSCustomObject]$ExistingTemplate = $null,
        [string]$TemplateName = '',
        [string]$BasePath
    )

    if ([string]::IsNullOrWhiteSpace($BasePath)) { $BasePath = Get-Win32ToolkitBasePath }
    $templateFolder = (Get-Win32ToolkitPaths -BasePath $BasePath).Templates
    if (-not (Test-Path $templateFolder)) {
        New-Item -ItemType Directory -Path $templateFolder -Force | Out-Null
    }

    # Detect the installed PSADT version to embed in the template. On a fresh machine PSADT is not
    # installed yet (it is pulled in later, during packaging), so this MUST tolerate its absence:
    # calling .Version.ToString() on a $null module throws "You cannot call a method on a null-valued
    # expression" and blocks the wizard at first run (issue #49). Bind the module first, then guard.
    $psadtModule = Get-Module -Name PSAppDeployToolkit -ListAvailable |
        Sort-Object Version -Descending | Select-Object -First 1
    $psadtVer = if ($psadtModule) { $psadtModule.Version.ToString() } else { 'unknown' }

    Write-Host ''
    Write-Host '=== Organisation Template Wizard ===' -ForegroundColor Cyan
    Write-Host 'Press Enter on any prompt to keep the default value.' -ForegroundColor DarkGray
    Write-Host ''

    function Read-TV { param([string]$P, [string]$D='') $s=if($D){"[$D] "}else{''}; $v=Read-Host "$P $s"; if([string]::IsNullOrWhiteSpace($v)){$D}else{$v.Trim()} }
    function Read-TB { param([string]$P, [bool]$D) $d=if($D){'y'}else{'n'}; $v=Read-Host "$P (y/n) [$d]"; if([string]::IsNullOrWhiteSpace($v)){$D}else{$v.Trim() -in @('y','Y','yes','Yes','1','true','True')} }
    function Read-TI { param([string]$P, [int]$D) $v=Read-Host "$P [$D]"; if([string]::IsNullOrWhiteSpace($v)){$D}else{$n=0;if([int]::TryParse($v.Trim(),[ref]$n)){$n}else{$D}} }

    Write-Host '--- A: Identity ---' -ForegroundColor Yellow
    $defaultTemplateName = if ($TemplateName) { $TemplateName } else { $ExistingTemplate?.TemplateName ?? 'Default' }
    $templateName = Read-TV 'Template name'                                    $defaultTemplateName
    $companyName  = Read-TV 'Company name (shown in dialog subtitles)'         ($ExistingTemplate?.CompanyName     ?? 'Your Organisation IT')
    $scriptAuthor = Read-TV 'App script author'                                ($ExistingTemplate?.AppScriptAuthor ?? 'IT Packaging Team')
    # Pinning the tenant is what lets publish REFUSE the wrong customer. Without it the toolkit cannot
    # know which tenant is correct and can only warn.
    Write-Host ' Pin this template to a customer tenant so publishing to the wrong one is refused.' -ForegroundColor DarkGray
    Write-Host ' Tenant GUID or domain (e.g. contoso.onmicrosoft.com). Blank = unpinned (not recommended).' -ForegroundColor DarkGray
    $tenantId     = Read-TV 'Intune tenant (blank = unpinned)'                 ($ExistingTemplate?.TenantId ?? '')

    Write-Host ''; Write-Host '--- B: Branding & dialog style ---' -ForegroundColor Yellow
    # DialogStyle (B2): Fluent = modern v4 look; Classic = v3-style dialogs (uses Banner.Classic.png).
    $dialogStyle = Read-TV 'Dialog style — Fluent or Classic'                    ($ExistingTemplate?.DialogStyle       ?? 'Fluent')
    if ($dialogStyle -notin @('Fluent','Classic')) {
        Write-Host " '$dialogStyle' is not valid — using 'Fluent'." -ForegroundColor DarkYellow
        $dialogStyle = 'Fluent'
    }
    $accentColor = Read-TV 'Fluent accent hex e.g. 0xFF0078D7 (blank=default)' ($ExistingTemplate?.FluentAccentColor ?? '')
    if (-not [string]::IsNullOrWhiteSpace($accentColor)) {
        $accentNorm = ConvertTo-Win32ToolkitAccentLiteral -Value $accentColor
        if ($accentNorm) { $accentColor = $accentNorm }
        else { Write-Host " '$accentColor' is not a valid hex colour — ignoring (PSADT default accent). Use 0xAARRGGBB, #RRGGBB, or RRGGBB." -ForegroundColor DarkYellow; $accentColor = '' }
    }
    $logPath     = Read-TV 'Log path'                                           ($ExistingTemplate?.LogPath           ?? '$envWinDir\Logs\Software')
    # LanguageOverride (C1): pin all PSADT dialogs to one UI language (e.g. nl, fr-FR, de).
    # Blank = auto-detect the signed-in user's language on-device (works under SYSTEM).
    $languageOverride = Read-TV 'Force dialog language e.g. nl / fr-FR (blank=auto-detect)' ($ExistingTemplate?.LanguageOverride ?? '')

    Write-Host ''; Write-Host '--- C: Progress Messages ---' -ForegroundColor Yellow
    $pMsgI = Read-TV 'Progress message - Install'   ($ExistingTemplate?.ProgressMessage?.Install   ?? 'Installation in progress. Please wait...')
    $pMsgR = Read-TV 'Progress message - Repair'    ($ExistingTemplate?.ProgressMessage?.Repair    ?? 'Repair in progress. Please wait...')
    $pMsgU = Read-TV 'Progress message - Uninstall' ($ExistingTemplate?.ProgressMessage?.Uninstall ?? 'Uninstallation in progress. Please wait...')
    $pDtlI = Read-TV 'Progress detail - Install'    ($ExistingTemplate?.ProgressMessageDetail?.Install   ?? 'This window will close automatically when the installation is complete.')
    $pDtlR = Read-TV 'Progress detail - Repair'     ($ExistingTemplate?.ProgressMessageDetail?.Repair    ?? 'This window will close automatically when the repair is complete.')
    $pDtlU = Read-TV 'Progress detail - Uninstall'  ($ExistingTemplate?.ProgressMessageDetail?.Uninstall ?? 'This window will close automatically when the uninstallation is complete.')

    Write-Host ''; Write-Host '--- C: Balloon Notifications ---' -ForegroundColor Yellow
    $balI = Read-TV 'Balloon complete - Install'   ($ExistingTemplate?.BalloonComplete?.Install   ?? 'Installation complete.')
    $balR = Read-TV 'Balloon complete - Repair'    ($ExistingTemplate?.BalloonComplete?.Repair    ?? 'Repair complete.')
    $balU = Read-TV 'Balloon complete - Uninstall' ($ExistingTemplate?.BalloonComplete?.Uninstall ?? 'Uninstallation complete.')

    Write-Host ''; Write-Host '--- D: Install Welcome Dialog ---' -ForegroundColor Yellow
    $wEnabled    = Read-TB 'Show Welcome dialog on install'              ($ExistingTemplate?.WelcomeDialog?.Enabled              ?? $true)
    $wDefer      = Read-TB 'Allow deferral'                              ($ExistingTemplate?.WelcomeDialog?.AllowDefer           ?? $true)
    $wDeferTimes = Read-TI 'Max deferrals'                               ($ExistingTemplate?.WelcomeDialog?.DeferTimes           ?? 3)
    $wDisk       = Read-TB 'Check disk space'                            ($ExistingTemplate?.WelcomeDialog?.CheckDiskSpace       ?? $false)
    $wPersist    = Read-TB 'Persist prompt (user cannot ignore)'         ($ExistingTemplate?.WelcomeDialog?.PersistPrompt        ?? $true)
    $wBlock      = Read-TB 'Block app re-launch during install'          ($ExistingTemplate?.WelcomeDialog?.BlockExecution       ?? $true)
    $wCountdown  = Read-TI 'Auto-close countdown seconds (0=off)'        ($ExistingTemplate?.WelcomeDialog?.CloseProcessesCountdown ?? 180)
    $wCustomText = Read-TB 'Show custom text from strings.psd1'          ($ExistingTemplate?.WelcomeDialog?.CustomText           ?? $false)

    Write-Host ''; Write-Host '--- D: Uninstall Welcome Dialog ---' -ForegroundColor Yellow
    Write-Host ' (shown only when processes need closing before uninstall)' -ForegroundColor DarkGray
    $uwEnabled   = Read-TB 'Show Welcome dialog on uninstall'            ($ExistingTemplate?.UninstallWelcomeDialog?.Enabled              ?? $true)
    $uwCountdown = Read-TI 'Auto-close countdown seconds (0=off)'        ($ExistingTemplate?.UninstallWelcomeDialog?.CloseProcessesCountdown ?? 60)
    $uwPersist   = Read-TB 'Persist prompt'                              ($ExistingTemplate?.UninstallWelcomeDialog?.PersistPrompt         ?? $false)
    $uwBlock     = Read-TB 'Block app re-launch during uninstall'        ($ExistingTemplate?.UninstallWelcomeDialog?.BlockExecution        ?? $false)

    Write-Host ''; Write-Host '--- D: Progress Dialog ---' -ForegroundColor Yellow
    $prEnabled = Read-TB  'Show Progress dialog'                                         ($ExistingTemplate?.ProgressDialog?.Enabled             ?? $true)
    $prStatus  = Read-TV  'Override status message (blank = use strings.psd1)'           ($ExistingTemplate?.ProgressDialog?.StatusMessage       ?? '')
    $prDetail  = Read-TV  'Override detail text, Fluent only (blank = use strings.psd1)' ($ExistingTemplate?.ProgressDialog?.StatusMessageDetail ?? '')

    Write-Host ''; Write-Host '--- D: Completion Prompt (Post-Install) ---' -ForegroundColor Yellow
    $cpEnabled = Read-TB 'Show completion prompt after install'  ($ExistingTemplate?.CompletionPrompt?.Enabled         ?? $false)
    $cpMessage = Read-TV 'Completion message'                   ($ExistingTemplate?.CompletionPrompt?.Message         ?? 'The installation has completed successfully.')
    $cpButton  = Read-TV 'Button label'                         ($ExistingTemplate?.CompletionPrompt?.ButtonRightText ?? 'OK')

    Write-Host ''; Write-Host '--- E: Org scripts & extension module (A1/A3) ---' -ForegroundColor Yellow
    $tplFolder = Join-Path $templateFolder $templateName
    Write-Host " Hook scripts run in every packaged app's deploy phases. Drop .ps1 files in:" -ForegroundColor DarkGray
    Write-Host " $tplFolder\Hooks\{PreInstall,PostInstall,PreUninstall,PostUninstall,PreRepair,PostRepair}.ps1" -ForegroundColor DarkGray
    Write-Host ' They run on-device under Windows PowerShell 5.1 — keep them 5.1-safe.' -ForegroundColor DarkGray
    $hooksEnabled = Read-TB 'Enable org hook scripts'                    ($ExistingTemplate?.Hooks?.Enabled ?? $false)
    $hooksFailure = if ($hooksEnabled) {
        $fa = Read-TV 'On hook error — Fail (stop the deploy) or Continue' ($ExistingTemplate?.Hooks?.FailureAction ?? 'Fail')
        if ($fa -notin @('Fail','Continue')) { Write-Host " '$fa' invalid — using 'Fail'." -ForegroundColor DarkYellow; 'Fail' } else { $fa }
    } else { ($ExistingTemplate?.Hooks?.FailureAction ?? 'Fail') }
    Write-Host " An org extension module (shared functions for your hooks) lives in:" -ForegroundColor DarkGray
    Write-Host " $tplFolder\PSAppDeployToolkit.<YourOrg>\ (auto-imported by PSADT in every project)" -ForegroundColor DarkGray
    $extModule = Read-TB 'Ship an org PSADT extension module'            ($ExistingTemplate?.ExtensionModule ?? $false)
    Write-Host " Org branding: drop AppIcon.png (dialogs + Intune tile fallback) and Banner.Classic.png in:" -ForegroundColor DarkGray
    Write-Host " $tplFolder\Assets\" -ForegroundColor DarkGray
    $customAssets = Read-TB 'Ship org branding assets (logo / banner)'   ($ExistingTemplate?.CustomAssets ?? $false)
    if ($hooksEnabled -or $extModule -or $customAssets) {
        if (-not (Test-Path $tplFolder)) { New-Item -ItemType Directory -Path $tplFolder -Force | Out-Null }
        if ($hooksEnabled  -and -not (Test-Path (Join-Path $tplFolder 'Hooks')))  { New-Item -ItemType Directory -Path (Join-Path $tplFolder 'Hooks')  -Force | Out-Null }
        if ($customAssets  -and -not (Test-Path (Join-Path $tplFolder 'Assets'))) { New-Item -ItemType Directory -Path (Join-Path $tplFolder 'Assets') -Force | Out-Null }
        Write-Host " ✓ Prepared $tplFolder — add your files there, then re-run the pipeline." -ForegroundColor DarkGreen
    }

    Write-Host ''; Write-Host '--- F: Intune publish defaults (D2/D3) ---' -ForegroundColor Yellow
    $idMinOS   = Read-TV 'Minimum Windows release (e.g. 1607, 22H2)'      ($ExistingTemplate?.IntuneDefaults?.MinimumWindowsRelease ?? '1607')
    $idRestart = Read-TV 'Device restart behavior (suppress/allow/force/basedOnReturnCode)' ($ExistingTemplate?.IntuneDefaults?.DeviceRestartBehavior ?? 'suppress')
    if ($idRestart -notin @('suppress','allow','force','basedOnReturnCode')) { Write-Host " '$idRestart' invalid — using 'suppress'." -ForegroundColor DarkYellow; $idRestart = 'suppress' }
    $idRuntime = Read-TI 'Max run time (minutes)'                          ($ExistingTemplate?.IntuneDefaults?.MaxRuntimeMinutes ?? 60)
    if ($idRuntime -le 0) { $idRuntime = 60 }
    $idDesc    = Read-TV 'Description boilerplate appended to every app (blank=none)' ($ExistingTemplate?.IntuneDefaults?.DescriptionBoilerplate ?? '')
    $idPrivacy = Read-TV 'Privacy information URL (blank=none)'            ($ExistingTemplate?.IntuneDefaults?.PrivacyUrl ?? '')
    $docFooter = Read-TV 'Customer-doc footer line (blank=default)'        ($ExistingTemplate?.DocFooter ?? '')

    $template = [PSCustomObject]@{
        TemplateSchemaVersion = $script:TemplateSchemaVersion
        TemplateName          = $templateName
        CompanyName           = $companyName
        AppScriptAuthor       = $scriptAuthor
        TenantId              = $tenantId
        DialogStyle           = $dialogStyle
        FluentAccentColor     = $accentColor
        LogPath               = $logPath
        LanguageOverride      = $languageOverride
        PsadtVersion          = $psadtVer
        ProgressMessage       = [PSCustomObject]@{ Install = $pMsgI; Repair = $pMsgR; Uninstall = $pMsgU }
        ProgressMessageDetail = [PSCustomObject]@{ Install = $pDtlI; Repair = $pDtlR; Uninstall = $pDtlU }
        BalloonComplete       = [PSCustomObject]@{ Install = $balI; Repair = $balR; Uninstall = $balU }
        WelcomeDialog         = [PSCustomObject]@{
            Enabled                 = $wEnabled
            AllowDefer              = $wDefer
            DeferTimes              = $wDeferTimes
            CheckDiskSpace          = $wDisk
            PersistPrompt           = $wPersist
            BlockExecution          = $wBlock
            CloseProcessesCountdown = $wCountdown
            CustomText              = $wCustomText
        }
        UninstallWelcomeDialog = [PSCustomObject]@{
            Enabled                 = $uwEnabled
            CloseProcessesCountdown = $uwCountdown
            PersistPrompt           = $uwPersist
            BlockExecution          = $uwBlock
        }
        ProgressDialog        = [PSCustomObject]@{
            Enabled             = $prEnabled
            StatusMessage       = $prStatus
            StatusMessageDetail = $prDetail
        }
        CompletionPrompt      = [PSCustomObject]@{
            Enabled         = $cpEnabled
            Message         = $cpMessage
            ButtonRightText = $cpButton
        }
        Hooks                 = [PSCustomObject]@{
            Enabled       = $hooksEnabled
            FailureAction = $hooksFailure
        }
        ExtensionModule       = $extModule
        CustomAssets          = $customAssets
        IntuneDefaults        = [PSCustomObject]@{
            MinimumWindowsRelease  = $idMinOS
            DeviceRestartBehavior  = $idRestart
            MaxRuntimeMinutes      = $idRuntime
            DescriptionBoilerplate = $idDesc
            PrivacyUrl             = $idPrivacy
        }
        DocFooter             = $docFooter
    }

    $savePath = Join-Path $templateFolder "$templateName.json"
    $template | ConvertTo-Json -Depth 10 | Set-Content -Path $savePath -Encoding UTF8
    Write-Host ''
    Write-Host "✓ Template '$templateName' saved: $savePath" -ForegroundColor Green
    return $template
}