PoshPresenter.psm1

#Requires -Version 7.0
<#
.SYNOPSIS
    PoshPresenter - PowerShell presentation engine with per-slide HTML architecture.

.DESCRIPTION
    Each slide is its own HTML file. The shell loads them in an iframe and handles
    navigation, timing, pace tracking, and speaker notes.

    USAGE:
        # Point at your presentation folder
        Start-PoshPresenter -Path "C:\MyTalks\ZeroTrustWorld"

        # Or point directly at the JSON file
        Start-PoshPresenter -Path "C:\MyTalks\ZeroTrustWorld\presentation.json"

        # Scaffold a new presentation
        New-PoshPresentation -Path "C:\MyTalks\NewTalk" -Title "My Talk"

    PRESENTATION FOLDER STRUCTURE:
        MyTalk\
          presentation.json <- your slide list, notes, durations
          slides\
            01-intro.html
            02-demo.html
          assets\ <- images, fonts, etc.

    presentation.json lives in YOUR folder and is never touched by module updates.

    Modes:
        standalone - Default, no sync
        presenter - Speaker notes, timer, pace tracker, broadcasts position
        attendee - Follows presenter sync

    Portable mode works without PoshDE installed.
#>


# ── Module State ──
$script:Listener  = $null
$script:RunLoop   = $null
$script:SyncState = @{ currentSlide = 0; isPresenter = $false }
$script:PublicRoot = Join-Path $PSScriptRoot 'Public'

# ──────────────────────────────────────────────────────────────────────────────
# Start-PoshPresenter
# ──────────────────────────────────────────────────────────────────────────────
function Start-PoshPresenter {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [switch]$Presenter,
        [switch]$Portable,
        [int]$Port = 0,
        [switch]$NoWindow,
        [string]$Title
    )

    # ── Resolve presentation directory and config file ──
    $resolved = Resolve-PresentationPath -Path $Path
    $presDir  = $resolved.Directory
    $presJson = $resolved.JsonFile

    # ── Load and validate presentation.json ──
    $config = Get-Content -Raw $presJson | ConvertFrom-Json
    if (-not $config.slides -or $config.slides.Count -eq 0) {
        throw "presentation.json has no slides defined: $presJson"
    }

    Write-Host ""
    Write-Host "PoshPresenter" -ForegroundColor Magenta -NoNewline
    Write-Host " | $($config.title)" -ForegroundColor White
    Write-Host " Slides : $($config.slides.Count)" -ForegroundColor DarkGray
    Write-Host " Duration : $($config.duration) min" -ForegroundColor DarkGray
    Write-Host " Folder : $presDir" -ForegroundColor DarkGray

    # ── Resolve port ──
    if ($Port -eq 0) {
        if (-not $Portable -and (Get-Command Register-PoshPort -ErrorAction SilentlyContinue)) {
            $Port = Register-PoshPort -App 'PoshPresenter'
            Write-Host " Port : $Port (PoshDE)" -ForegroundColor DarkGray
        } else {
            $Port = Find-FreePort
            Write-Host " Port : $Port (auto)" -ForegroundColor DarkGray
        }
    }

    # ── Resolve theme ──
    $themeCSS = $null
    if (-not $Portable -and (Get-Command Export-PoshThemeCSS -ErrorAction SilentlyContinue)) {
        try { $themeCSS = Export-PoshThemeCSS } catch {}
    }

    # ── MIME types ──
    $mimeTypes = @{
        '.html' = 'text/html'
        '.css'  = 'text/css'
        '.js'   = 'application/javascript'
        '.json' = 'application/json'
        '.png'  = 'image/png'
        '.jpg'  = 'image/jpeg'
        '.jpeg' = 'image/jpeg'
        '.gif'  = 'image/gif'
        '.svg'  = 'image/svg+xml'
        '.ico'  = 'image/x-icon'
        '.webp' = 'image/webp'
        '.mp4'  = 'video/mp4'
        '.woff' = 'font/woff'
        '.woff2'= 'font/woff2'
        '.ttf'  = 'font/ttf'
    }

    # ── Sync state ──
    $script:SyncState = @{ currentSlide = 0; isPresenter = [bool]$Presenter }

    # ── Start HTTP listener ──
    $prefix = "http://localhost:$Port/"
    $script:Listener = New-Object System.Net.HttpListener
    $script:Listener.Prefixes.Add($prefix)

    try {
        $script:Listener.Start()
    } catch {
        Write-Warning "Port $Port unavailable. Trying another..."
        $Port = Find-FreePort
        $prefix = "http://localhost:$Port/"
        $script:Listener = New-Object System.Net.HttpListener
        $script:Listener.Prefixes.Add($prefix)
        $script:Listener.Start()
    }

    $mode   = if ($Presenter) { 'presenter' } else { 'standalone' }
    $appUrl = "http://localhost:$Port/?mode=$mode"
    Write-Host " URL : " -ForegroundColor DarkGray -NoNewline
    Write-Host $appUrl -ForegroundColor Cyan
    Write-Host ""

    # ── Serialize presentation config for the API ──
    # Read fresh from disk so runtime edits are picked up on reload
    $configJson = Get-Content -Raw $presJson

    # ── Request handler (runs in background runspace) ──
    $script:RunLoop = [PowerShell]::Create().AddScript({
        param($Listener, $PublicRoot, $PresDir, $ConfigJson, $SyncState, $ThemeCSS, $MimeTypes)

        while ($Listener.IsListening) {
            try {
                $ctx  = $Listener.GetContext()
                $req  = $ctx.Request
                $res  = $ctx.Response
                $path = $req.Url.AbsolutePath

                # CORS / cache headers
                $res.Headers.Add("Access-Control-Allow-Origin", "*")
                $res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
                $res.Headers.Add("Access-Control-Allow-Headers", "Content-Type")
                $res.Headers.Add("Cache-Control", "no-cache")

                if ($req.HttpMethod -eq 'OPTIONS') {
                    $res.StatusCode = 204; $res.Close(); continue
                }

                # ── /api/presentation ──
                # Returns the presentation.json — no file writing, just reads what was loaded
                if ($path -eq '/api/presentation') {
                    $bytes = [System.Text.Encoding]::UTF8.GetBytes($ConfigJson)
                    $res.ContentType = 'application/json'
                    $res.OutputStream.Write($bytes, 0, $bytes.Length)
                    $res.Close(); continue
                }

                # ── /api/theme ──
                if ($path -eq '/api/theme') {
                    $obj   = @{ css = if ($ThemeCSS) { $ThemeCSS } else { $null } }
                    $bytes = [System.Text.Encoding]::UTF8.GetBytes(($obj | ConvertTo-Json))
                    $res.ContentType = 'application/json'
                    $res.OutputStream.Write($bytes, 0, $bytes.Length)
                    $res.Close(); continue
                }

                # ── /api/sync GET ──
                if ($path -eq '/api/sync' -and $req.HttpMethod -eq 'GET') {
                    $bytes = [System.Text.Encoding]::UTF8.GetBytes(($SyncState | ConvertTo-Json))
                    $res.ContentType = 'application/json'
                    $res.OutputStream.Write($bytes, 0, $bytes.Length)
                    $res.Close(); continue
                }

                # ── /api/sync POST ──
                if ($path -eq '/api/sync' -and $req.HttpMethod -eq 'POST') {
                    $reader = New-Object System.IO.StreamReader($req.InputStream)
                    $body   = $reader.ReadToEnd() | ConvertFrom-Json
                    $reader.Close()
                    if ($null -ne $body.currentSlide) {
                        $SyncState['currentSlide'] = [int]$body.currentSlide
                    }
                    $bytes = [System.Text.Encoding]::UTF8.GetBytes('{"ok":true}')
                    $res.ContentType = 'application/json'
                    $res.OutputStream.Write($bytes, 0, $bytes.Length)
                    $res.Close(); continue
                }

                # ── /api/execute POST ──
                if ($path -eq '/api/execute' -and $req.HttpMethod -eq 'POST') {
                    $reader = New-Object System.IO.StreamReader($req.InputStream)
                    $body   = $reader.ReadToEnd() | ConvertFrom-Json
                    $reader.Close()

                    $result = @{ output = ''; error = ''; success = $true }
                    try {
                        $ps = [PowerShell]::Create()
                        $ps.AddScript($body.command) | Out-Null
                        $output = $ps.Invoke()
                        $result.output = ($output | Out-String).TrimEnd()
                        if ($ps.Streams.Error.Count -gt 0) {
                            $result.error   = ($ps.Streams.Error | Out-String).TrimEnd()
                            $result.success = $false
                        }
                        if ($ps.Streams.Warning.Count -gt 0) {
                            $result.output += "`n" + ($ps.Streams.Warning | Out-String).TrimEnd()
                        }
                        $ps.Dispose()
                    } catch {
                        $result.error   = $_.Exception.Message
                        $result.success = $false
                    }

                    $bytes = [System.Text.Encoding]::UTF8.GetBytes(($result | ConvertTo-Json -Depth 3))
                    $res.ContentType = 'application/json'
                    $res.OutputStream.Write($bytes, 0, $bytes.Length)
                    $res.Close(); continue
                }

                # ── Static file routing ──
                # Default root → shell.html (the presentation UI)
                if ($path -eq '/') { $path = '/index.html' }

                # File resolution order:
                # 1. Module Public folder (shell.html, shell.js, shell.css, slide-base.*)
                # 2. Presentation folder (slides/, assets/, any user files)
                $filePath = Join-Path $PublicRoot $path.TrimStart('/')
                if (-not (Test-Path $filePath -PathType Leaf)) {
                    $filePath = Join-Path $PresDir $path.TrimStart('/')
                }

                if (Test-Path $filePath -PathType Leaf) {
                    $ext = [System.IO.Path]::GetExtension($filePath).ToLower()
                    $res.ContentType = if ($MimeTypes.ContainsKey($ext)) { $MimeTypes[$ext] } else { 'application/octet-stream' }
                    $fileBytes = [System.IO.File]::ReadAllBytes($filePath)
                    $res.OutputStream.Write($fileBytes, 0, $fileBytes.Length)
                } else {
                    $res.StatusCode = 404
                    $msg = [System.Text.Encoding]::UTF8.GetBytes("404 Not Found: $path")
                    $res.OutputStream.Write($msg, 0, $msg.Length)
                }

                $res.Close()
            } catch [System.Net.HttpListenerException] {
                break
            } catch {
                try { $res.StatusCode = 500; $res.Close() } catch {}
            }
        }
    }).AddArgument($script:Listener
    ).AddArgument($script:PublicRoot
    ).AddArgument($presDir
    ).AddArgument($configJson
    ).AddArgument($script:SyncState
    ).AddArgument($themeCSS
    ).AddArgument($mimeTypes)

    $script:RunLoop.BeginInvoke() | Out-Null
    Write-Host " Server: " -ForegroundColor DarkGray -NoNewline
    Write-Host "running" -ForegroundColor Green

    # ── Launch window ──
    if (-not $NoWindow) {
        $windowTitle = if ($Title) { $Title } elseif ($config.title) { $config.title } else { 'PoshPresenter' }
        Start-PoshPresenterWindow -Url $appUrl -Title $windowTitle -Portable:$Portable
    }

    [PSCustomObject]@{
        Port     = $Port
        Url      = $appUrl
        Mode     = $mode
        Slides   = $config.slides.Count
        Duration = $config.duration
        Folder   = $presDir
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# Stop-PoshPresenter
# ──────────────────────────────────────────────────────────────────────────────
function Stop-PoshPresenter {
    [CmdletBinding()]
    param()

    if ($script:Listener -and $script:Listener.IsListening) {
        $script:Listener.Stop()
        $script:Listener.Close()
        Write-Host "PoshPresenter stopped." -ForegroundColor Yellow
    }
    if ($script:RunLoop) {
        $script:RunLoop.Stop()
        $script:RunLoop.Dispose()
        $script:RunLoop = $null
    }
    if (Get-Command Unregister-PoshPort -ErrorAction SilentlyContinue) {
        try { Unregister-PoshPort -App 'PoshPresenter' } catch {}
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# New-PoshPresentation
# Scaffolds a fresh presentation folder so users are instantly ready to go.
# ──────────────────────────────────────────────────────────────────────────────
function New-PoshPresentation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [string]$Title  = 'My Presentation',
        [string]$Author = $env:USERNAME,
        [int]$Duration  = 30
    )

    # Expand ~ etc.
    $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

    if (Test-Path $Path) {
        # Allow pointing at an existing empty folder
        $existing = Get-ChildItem $Path -ErrorAction SilentlyContinue
        if ($existing) {
            throw "Folder already exists and is not empty: $Path`nDelete it first or choose a new path."
        }
    }

    # Create folder structure
    New-Item -ItemType Directory -Path (Join-Path $Path 'slides')  -Force | Out-Null
    New-Item -ItemType Directory -Path (Join-Path $Path 'assets')  -Force | Out-Null

    # ── presentation.json ──
    $config = [ordered]@{
        title    = $Title
        author   = $Author
        duration = $Duration
        slides   = @(
            [ordered]@{
                file     = 'slides/01-title.html'
                title    = 'Title'
                section  = 'Introduction'
                duration = 1
                notes    = 'Opening slide. Introduce yourself and the topic.'
            }
            [ordered]@{
                file     = 'slides/02-demo.html'
                title    = 'Demo'
                section  = 'Demo'
                duration = 5
                notes    = 'Live demo goes here.'
            }
        )
    }
    $config | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $Path 'presentation.json') -Encoding UTF8

    # ── Starter slide 1: title ──
    $titleSlide = @'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/slide-base.css">
    <script src="/slide-base.js"></script>
    <style>
        body {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
            margin: 0;
            background: #0a0a0a;
            font-family: 'Courier New', monospace;
        }
        .center {
            text-align: center;
        }
        h1 {
            font-size: 3rem;
            color: #00ff9f;
            margin: 0 0 1rem;
        }
        p {
            color: #888;
            font-size: 1.2rem;
        }
    </style>
</head>
<body>
    <div class="center">
        <h1>SLIDE_TITLE_PLACEHOLDER</h1>
        <p>Edit slides/01-title.html to customize this slide.</p>
        <p>See <a href="https://powershellforhackers.com" style="color:#00ff9f">powershellforhackers.com</a> for examples.</p>
    </div>
</body>
</html>
'@

    $titleSlide = $titleSlide -replace 'SLIDE_TITLE_PLACEHOLDER', $Title
    $titleSlide | Set-Content (Join-Path $Path 'slides/01-title.html') -Encoding UTF8

    # ── Starter slide 2: live code demo ──
    $demoSlide = @'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/slide-base.css">
    <script src="/slide-base.js"></script>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
            margin: 0;
            background: #0a0a0a;
            font-family: 'Courier New', monospace;
            color: #e0e0e0;
        }
        h2 { color: #00ff9f; margin-bottom: 1.5rem; }
        pre {
            background: #111;
            border: 1px solid #333;
            border-radius: 6px;
            padding: 1rem 1.5rem;
            font-size: 0.95rem;
            color: #00ff9f;
            margin-bottom: 1rem;
        }
    </style>
</head>
<body>
    <h2>Live Code Execution Demo</h2>

    <!-- posh-run lets you run PowerShell inline. Remove the data-attendee-hide
         attribute if you want attendees to also see the Run button. -->
    <pre>Get-ComputerInfo | Select-Object WindowsProductName, OsArchitecture</pre>

    <div class="posh-run"
         data-code="Get-ComputerInfo | Select-Object WindowsProductName, OsArchitecture | Format-List"
         data-label="▶ Run on presenter machine">
    </div>

    <p style="color:#555; margin-top:2rem; font-size:0.85rem">
        Edit slides/02-demo.html to customize this slide.
    </p>
</body>
</html>
'@

    $demoSlide | Set-Content (Join-Path $Path 'slides/02-demo.html') -Encoding UTF8

    # ── README ──
    $readme = @"
# $Title

Created with PoshPresenter.

## Quick Start

``````powershell
Import-Module PoshPresenter
Start-PoshPresenter -Path "$Path"

# Presenter mode (speaker notes + timer)
Start-PoshPresenter -Path "$Path" -Presenter
``````

## Folder Structure

    $Title\
      presentation.json <- slide list, notes, durations
      slides\ <- your HTML slides (one file per slide)
      assets\ <- images, fonts, videos

## Adding Slides

1. Create a new HTML file in slides\ (e.g. slides\03-attack.html)
2. Add it to presentation.json:

``````json
{
  "file": "slides/03-attack.html",
  "title": "The Attack",
  "section": "Demo",
  "duration": 3,
  "notes": "Speaker notes go here."
}
``````

## Slide Helpers

Each slide can include:
  - /slide-base.css <- dark theme, code blocks, utility classes
  - /slide-base.js <- Posh.run() for live code execution

These are served from the PoshPresenter module automatically.

## Resources

- powershellforhackers.com
- github.com/jakoby
"@

    $readme | Set-Content (Join-Path $Path 'README.md') -Encoding UTF8

    # ── Done ──
    Write-Host ""
    Write-Host "PoshPresenter" -ForegroundColor Magenta -NoNewline
    Write-Host " | New presentation scaffolded" -ForegroundColor Green
    Write-Host " Folder : $Path" -ForegroundColor DarkGray
    Write-Host " Config : presentation.json" -ForegroundColor DarkGray
    Write-Host " Slides : $($config.slides.Count) starter slides" -ForegroundColor DarkGray
    Write-Host ""
    Write-Host " Next:" -ForegroundColor White
    Write-Host " Start-PoshPresenter -Path `"$Path`"" -ForegroundColor Cyan
    Write-Host " Start-PoshPresenter -Path `"$Path`" -Presenter" -ForegroundColor Cyan
    Write-Host ""

    [PSCustomObject]@{
        Path   = $Path
        Config = Join-Path $Path 'presentation.json'
        Slides = Join-Path $Path 'slides'
        Assets = Join-Path $Path 'assets'
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# Internal helpers
# ──────────────────────────────────────────────────────────────────────────────

function Resolve-PresentationPath {
    param([string]$Path)

    $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

    if (-not (Test-Path $Path)) {
        throw "Path not found: $Path"
    }

    # If they passed a .json file directly
    if ((Get-Item $Path).PSIsContainer -eq $false) {
        if ($Path -notlike '*.json') {
            throw "File must be a .json presentation config: $Path"
        }
        return [PSCustomObject]@{
            Directory = Split-Path $Path -Parent
            JsonFile  = $Path
        }
    }

    # It's a directory — look for presentation.json
    $jsonFile = Join-Path $Path 'presentation.json'
    if (-not (Test-Path $jsonFile)) {
        # Friendly error with hint
        throw @"
No presentation.json found in: $Path

Create one with:
    New-PoshPresentation -Path "$Path" -Title "My Talk"

Or point at an existing presentation.json file directly:
    Start-PoshPresenter -Path "C:\MyTalks\MyTalk\presentation.json"
"@

    }

    return [PSCustomObject]@{
        Directory = $Path
        JsonFile  = $jsonFile
    }
}

function Start-PoshPresenterWindow {
    param([string]$Url, [string]$Title, [switch]$Portable)

    # Option 1: PoshDE WebView2 via PS5.1 subprocess
    if (-not $Portable -and (Get-Command Get-WebView2DllPath -ErrorAction SilentlyContinue)) {
        $dllPath = Get-WebView2DllPath
        if ($dllPath -and (Test-Path $dllPath)) {
            Write-Host " Window : PoshDE WebView2" -ForegroundColor DarkGray
            $escapedDll   = $dllPath   -replace "'", "''"
            $escapedUrl   = $Url       -replace "'", "''"
            $escapedTitle = $Title     -replace "'", "''"
            $userDataFolder = Join-Path $env:LOCALAPPDATA 'PoshPresenter\WebView2'

            $ps51 = @"
try {
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
    Add-Type -Path '$escapedDll\Microsoft.Web.WebView2.Core.dll'
    Add-Type -Path '$escapedDll\Microsoft.Web.WebView2.WinForms.dll'
    [System.Windows.Forms.Application]::EnableVisualStyles()

    `$userDataFolder = '$($userDataFolder -replace "'", "''")'
    if (-not (Test-Path `$userDataFolder)) {
        New-Item -Path `$userDataFolder -ItemType Directory -Force | Out-Null
    }

    `$form = New-Object System.Windows.Forms.Form
    `$form.Text = '$escapedTitle — PoshPresenter'
    `$form.Width = 1280; `$form.Height = 800
    `$form.StartPosition = 'CenterScreen'
    `$form.BackColor = [System.Drawing.Color]::FromArgb(10,10,10)

    `$wv = New-Object Microsoft.Web.WebView2.WinForms.WebView2
    `$wv.Dock = 'Fill'
    `$wv.DefaultBackgroundColor = [System.Drawing.Color]::FromArgb(10,10,10)

    `$creationProps = New-Object Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties
    `$creationProps.UserDataFolder = `$userDataFolder
    `$wv.CreationProperties = `$creationProps
    `$form.Controls.Add(`$wv)

    `$wv.Add_CoreWebView2InitializationCompleted({
        param(`$sender, `$e)
        if (`$e.IsSuccess) {
            `$wv.CoreWebView2.Navigate('$escapedUrl')
            `$wv.CoreWebView2.add_ContainsFullScreenElementChanged({
                if (`$wv.CoreWebView2.ContainsFullScreenElement) {
                    `$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
                    `$form.WindowState = [System.Windows.Forms.FormWindowState]::Maximized
                } else {
                    `$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
                    `$form.WindowState = [System.Windows.Forms.FormWindowState]::Normal
                }
            })
        }
    })
    `$form.Add_Shown({ `$wv.EnsureCoreWebView2Async(`$null) })
    `$form.Add_FormClosing({ try { `$wv.Dispose() } catch {} })
    [System.Windows.Forms.Application]::Run(`$form)
} catch {
    [System.Windows.Forms.MessageBox]::Show(`$_.Exception.Message, 'PoshPresenter Error')
}
"@

            $tempScript = Join-Path $env:TEMP "PoshPresenter_$(Get-Random).ps1"
            $ps51 | Set-Content -Path $tempScript -Encoding UTF8
            Start-Process powershell.exe -ArgumentList @(
                '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$tempScript`""
            )
            return
        }
    }

    # Option 2: PoshWebView.exe bundled with the module
    $webviewExe = Join-Path $PSScriptRoot 'PoshWebView.exe'
    if (Test-Path $webviewExe) {
        Write-Host " Window : PoshWebView.exe" -ForegroundColor DarkGray
        Start-Process -FilePath $webviewExe -ArgumentList $Url, $Title, '1280', '800'
        return
    }

    # Option 3: Browser fallback (least preferred)
    Write-Host " Window : Default browser (install PoshDE for WebView2)" -ForegroundColor Yellow
    Start-Process $Url
}

function Find-FreePort {
    $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
    $listener.Start()
    $port = $listener.LocalEndpoint.Port
    $listener.Stop()
    return $port
}

# ──────────────────────────────────────────────────────────────────────────────
# Start-PoshDemo
# Launches the built-in interactive demo that teaches users how to build slides.
# ──────────────────────────────────────────────────────────────────────────────
function Start-PoshDemo {
    [CmdletBinding()]
    param(
        [switch]$Presenter,
        [switch]$Portable,
        [switch]$NoWindow
    )

    $demoPath = Join-Path $PSScriptRoot 'Demo'

    if (-not (Test-Path $demoPath)) {
        throw "Demo folder not found at: $demoPath`nThis may indicate a corrupted module installation."
    }

    Write-Host ""
    Write-Host "PoshPresenter" -ForegroundColor Magenta -NoNewline
    Write-Host " | Launching built-in demo..." -ForegroundColor DarkGray

    Start-PoshPresenter -Path $demoPath -Presenter:$Presenter -Portable:$Portable -NoWindow:$NoWindow
}

# ── Exports ──
Export-ModuleMember -Function Start-PoshPresenter, Stop-PoshPresenter, New-PoshPresentation, Start-PoshDemo