From d487751380579411e907b8c4df16c542cf97475f Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:52:25 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Demo=20output=20beautify:=20readable=20Vali?= =?UTF-8?q?date=20=E2=86=92=20Plan=20=E2=86=92=20Execute=20(examples=20onl?= =?UTF-8?q?y)=20Fixes=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/Invoke-IdleDemo.ps1 | 280 +++++++++++++++++++++++++++++++++++ examples/run-demo.ps1 | 116 --------------- 2 files changed, 280 insertions(+), 116 deletions(-) create mode 100644 examples/Invoke-IdleDemo.ps1 delete mode 100644 examples/run-demo.ps1 diff --git a/examples/Invoke-IdleDemo.ps1 b/examples/Invoke-IdleDemo.ps1 new file mode 100644 index 00000000..9d092a02 --- /dev/null +++ b/examples/Invoke-IdleDemo.ps1 @@ -0,0 +1,280 @@ +#requires -Version 7.0 + +[CmdletBinding(DefaultParameterSetName = 'Run')] +param( + [Parameter(ParameterSetName = 'List')] + [switch]$List, + + [Parameter(ParameterSetName = 'Run')] + [string[]]$Example, + + [Parameter(ParameterSetName = 'Run')] + [switch]$All, + + [Parameter(ParameterSetName = 'Run')] + [ValidateRange(1, 50)] + [int]$Repeat = 1, + + [Parameter(ParameterSetName = 'Run')] + [switch]$FailFast, + + [Parameter(ParameterSetName = 'Run')] + [switch]$NoColor +) + +Set-StrictMode -Version Latest + +function Test-IdleAnsiSupport { + try { + return ($Host.UI.SupportsVirtualTerminal -or ($env:TERM -and $env:TERM -ne 'dumb')) + } catch { return $false } +} + +function Test-IdleInteractiveHost { + try { + if (-not [Environment]::UserInteractive) { return $false } + if ([Console]::IsInputRedirected) { return $false } + if ($env:CI) { return $false } + if (-not $Host.UI) { return $false } + if (-not $Host.UI.RawUI) { return $false } + return $true + } catch { + return $false + } +} + +$UseAnsi = if ($NoColor) { $false } else { Test-IdleAnsiSupport } + +function Write-DemoHeader { + param([Parameter(Mandatory)][string]$Title) + + if ($UseAnsi) { + Write-Host "$($PSStyle.Bold)$($PSStyle.Foreground.Cyan)$Title$($PSStyle.Reset)" + } else { + Write-Host $Title + } +} + +function Format-EventRow { + param([Parameter(Mandatory)][object]$Event) + + $icons = @{ + RunStarted = '🚀' + RunCompleted = '🏁' + StepStarted = 'â–ļī¸' + StepCompleted = '✅' + StepSkipped = 'â­ī¸' + StepFailed = '❌' + Custom = '📝' + Debug = '🔎' + } + + $icon = if ($icons.ContainsKey($Event.Type)) { $icons[$Event.Type] } else { 'â€ĸ' } + + $time = ([DateTime]$Event.TimestampUtc).ToLocalTime().ToString('HH:mm:ss.fff') + $step = if ([string]::IsNullOrWhiteSpace($Event.StepName)) { '-' } else { [string]$Event.StepName } + + $msg = [string]$Event.Message + + # IMPORTANT: Show error details if the engine attached them. + if ($Event.PSObject.Properties.Name -contains 'Data' -and $Event.Data -is [hashtable]) { + if ($Event.Data.ContainsKey('Error') -and -not [string]::IsNullOrWhiteSpace([string]$Event.Data.Error)) { + $msg = "$msg | ERROR: $([string]$Event.Data.Error)" + } + } + + [pscustomobject]@{ + Time = $time + Type = "$icon $($Event.Type)" + Step = $step + Message = $msg + } +} + +function Write-ResultSummary { + param([Parameter(Mandatory)][object]$Result) + + $statusIcon = switch ($Result.Status) { + 'Completed' { '✅' } + 'Failed' { '❌' } + default { 'â„šī¸' } + } + + if ($UseAnsi) { + $color = if ($Result.Status -eq 'Completed') { $PSStyle.Foreground.Green } else { $PSStyle.Foreground.Red } + Write-Host "$($PSStyle.Bold)$statusIcon Status: $color$($Result.Status)$($PSStyle.Reset)" + } else { + Write-Host "$statusIcon Status: $($Result.Status)" + } + + $counts = $Result.Events | Group-Object Type | Sort-Object Name | ForEach-Object { "$($_.Name)=$($_.Count)" } + Write-Host ("Events: " + ($counts -join ', ')) +} + +function Get-IdleLifecycleEventFromWorkflowName { + param([Parameter(Mandatory)][string]$Name) + + $n = $Name.ToLowerInvariant() + + if ($n.StartsWith('joiner')) { return 'Joiner' } + if ($n.StartsWith('mover')) { return 'Mover' } + if ($n.StartsWith('leaver')) { return 'Leaver' } + + # Safe default for demos until workflows carry metadata for lifecycle event. + return 'Joiner' +} + +function Get-DemoWorkflows { + $workflowDir = Join-Path -Path $PSScriptRoot -ChildPath 'workflows' + + if (-not (Test-Path -Path $workflowDir)) { + throw "Workflow directory not found: $workflowDir" + } + + Get-ChildItem -Path $workflowDir -Filter '*.psd1' -File | + Sort-Object Name | + ForEach-Object { + [pscustomobject]@{ + Name = $_.BaseName + Path = $_.FullName + File = $_.Name + } + } +} + +function Select-DemoWorkflows { + param( + [Parameter(Mandatory)] + [object[]]$AvailableWorkflows, + + [string[]]$ExampleNames, + + [switch]$AllWorkflows + ) + + if ($AllWorkflows) { return $AvailableWorkflows } + + if ($ExampleNames -and $ExampleNames.Count -gt 0) { + $lookup = @{} + foreach ($wf in $AvailableWorkflows) { + $lookup[$wf.Name.ToLowerInvariant()] = $wf + } + + $selected = foreach ($name in $ExampleNames) { + $key = $name.ToLowerInvariant() + if (-not $lookup.ContainsKey($key)) { + $available = ($AvailableWorkflows | Select-Object -ExpandProperty Name) -join ', ' + throw "Unknown example '$name'. Available: $available" + } + $lookup[$key] + } + + return $selected + } + + # Minimal interactive fallback (only if no parameters were provided). + if (Test-IdleInteractiveHost) { + Write-Host "" + Write-DemoHeader "IdLE Demo – Select Example" + $i = 1 + foreach ($wf in $AvailableWorkflows) { + Write-Host ("[{0}] {1}" -f $i, $wf.Name) + $i++ + } + + Write-Host "" + $choiceRaw = Read-Host "Select an example (1-$($AvailableWorkflows.Count))" + + # IMPORTANT: [ref] requires an existing variable in StrictMode. + $choice = 0 + if ([int]::TryParse($choiceRaw, [ref]$choice) -and $choice -ge 1 -and $choice -le $AvailableWorkflows.Count) { + return @($AvailableWorkflows[$choice - 1]) + } + + Write-Host "Invalid selection. Using default: $($AvailableWorkflows[0].Name)" + return @($AvailableWorkflows[0]) + } + + # Non-interactive default + return @($AvailableWorkflows[0]) +} + +# Import modules from the repo (path-based import, no global installation required). +Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop +Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop +Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop + +$available = @(Get-DemoWorkflows) + +if ($available.Count -eq 0) { + throw "No workflows found in 'examples/workflows'." +} + +if ($List) { + Write-DemoHeader "IdLE Demo – Available Examples" + $available | Select-Object Name, File | Format-Table -AutoSize + return +} + +$selected = @(Select-DemoWorkflows -AvailableWorkflows $available -ExampleNames $Example -AllWorkflows:$All) + +$providers = @{ + Identity = New-IdleMockIdentityProvider +} + +$allResults = @() + +foreach ($wf in $selected) { + for ($r = 1; $r -le $Repeat; $r++) { + Write-Host "" + Write-DemoHeader ("IdLE Demo – {0} (run {1}/{2})" -f $wf.Name, $r, $Repeat) + + Write-Host "" + Write-DemoHeader "Validate" + Test-IdleWorkflow -WorkflowPath $wf.Path | Out-Null + Write-Host "✅ Workflow OK: $($wf.File)" + + Write-Host "" + Write-DemoHeader "Plan" + $lifecycleEvent = Get-IdleLifecycleEventFromWorkflowName -Name $wf.Name + $request = New-IdleLifecycleRequest -LifecycleEvent $lifecycleEvent -Actor 'example-user' + $plan = New-IdlePlan -WorkflowPath $wf.Path -Request $request + Write-Host ("Plan created: LifecycleEvent={0} | Steps={1}" -f $lifecycleEvent, ($plan.Steps | Measure-Object).Count) + + Write-Host "" + Write-DemoHeader "Execute" + $result = Invoke-IdlePlan -Plan $plan -Providers $providers + $allResults += $result + + Write-Host "" + Write-DemoHeader "Step Results" + $result.Steps | + Select-Object Name, Type, Status, + @{ Name = 'Changed'; Expression = { if ($_.PSObject.Properties.Name -contains 'Changed') { $_.Changed } else { $null } } }, + Error | + Format-Table -AutoSize + + Write-Host "" + Write-DemoHeader "Event Stream" + $result.Events | + ForEach-Object { Format-EventRow $_ } | + Format-Table Time, Type, Step, Message -AutoSize + + Write-Host "" + Write-ResultSummary -Result $result + + if ($FailFast -and $result.Status -ne 'Completed') { + throw "FailFast: execution failed for '$($wf.Name)'." + } + } +} + +if ($selected.Count -gt 1 -or $Repeat -gt 1) { + Write-Host "" + Write-DemoHeader "Overall Summary" + $allResults | + Group-Object Status | + Sort-Object Name | + ForEach-Object { [pscustomobject]@{ Status = $_.Name; Count = $_.Count } } | + Format-Table -AutoSize +} diff --git a/examples/run-demo.ps1 b/examples/run-demo.ps1 deleted file mode 100644 index dea29656..00000000 --- a/examples/run-demo.ps1 +++ /dev/null @@ -1,116 +0,0 @@ -#requires -Version 7.0 -Set-StrictMode -Version Latest - -function Test-IdleAnsiSupport { - try { - return ($Host.UI.SupportsVirtualTerminal -or $env:TERM -and $env:TERM -ne 'dumb') - } catch { return $false } -} - -$UseAnsi = Test-IdleAnsiSupport - -function Write-DemoHeader { - param([string]$Title) - - if ($UseAnsi) { - Write-Host "$($PSStyle.Bold)$($PSStyle.Foreground.Cyan)$Title$($PSStyle.Reset)" - } else { - Write-Host $Title - } -} - -function Format-EventRow { - param([object]$Event) - - $icons = @{ - RunStarted = '🚀' - RunCompleted = '🏁' - StepStarted = 'â–ļī¸' - StepCompleted = '✅' - StepSkipped = 'â­ī¸' - StepFailed = '❌' - Custom = '📝' - Debug = '🔎' - } - - $icon = if ($icons.ContainsKey($Event.Type)) { $icons[$Event.Type] } else { 'â€ĸ' } - - $time = ([DateTime]$Event.TimestampUtc).ToLocalTime().ToString('HH:mm:ss.fff') - $step = if ([string]::IsNullOrWhiteSpace($Event.StepName)) { '-' } else { [string]$Event.StepName } - - $msg = [string]$Event.Message - - # IMPORTANT: Show error details if the engine attached them. - if ($Event.PSObject.Properties.Name -contains 'Data' -and $Event.Data -is [hashtable]) { - if ($Event.Data.ContainsKey('Error') -and -not [string]::IsNullOrWhiteSpace([string]$Event.Data.Error)) { - $msg = "$msg | ERROR: $([string]$Event.Data.Error)" - } - } - - [pscustomobject]@{ - Time = $time - Type = "$icon $($Event.Type)" - Step = $step - Message = $msg - } -} - -function Write-ResultSummary { - param([object]$Result) - - $statusIcon = switch ($Result.Status) { - 'Completed' { '✅' } - 'Failed' { '❌' } - default { 'â„šī¸' } - } - - if ($UseAnsi) { - $color = if ($Result.Status -eq 'Completed') { $PSStyle.Foreground.Green } else { $PSStyle.Foreground.Red } - Write-Host "$($PSStyle.Bold)$statusIcon Status: $color$($Result.Status)$($PSStyle.Reset)" - } else { - Write-Host "$statusIcon Status: $($Result.Status)" - } - - $counts = $Result.Events | Group-Object Type | Sort-Object Name | ForEach-Object { "$($_.Name)=$($_.Count)" } - Write-Host ("Events: " + ($counts -join ', ')) -} - -# Import modules from the repo (path-based import, no global installation required). -Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop -Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop -Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop - -# Select demo workflow. -$workflowPath = Join-Path -Path $PSScriptRoot -ChildPath 'workflows\joiner-minimal-ensureattribute.psd1' - -# Validate workflow early for clear errors. -Test-IdleWorkflow -WorkflowPath $workflowPath | Out-Null - -# Create request and plan. -$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user' -$plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request - -# Host-provided providers. -$providers = @{ - Identity = New-IdleMockIdentityProvider -} - -$result = Invoke-IdlePlan -Plan $plan -Providers $providers - -Write-DemoHeader "IdLE Demo – Plan Execution" - -Write-Host "" -Write-DemoHeader "Step Results" -$result.Steps | - Select-Object Name, Type, Status, - @{ Name = 'Changed'; Expression = { if ($_.PSObject.Properties.Name -contains 'Changed') { $_.Changed } else { $null } } }, - Error | - Format-Table -AutoSize - -Write-Host "" -Write-DemoHeader "Event Stream" -$result.Events | - ForEach-Object { Format-EventRow $_ } | - Format-Table Time, Type, Step, Message -AutoSize - -Write-ResultSummary -Result $result \ No newline at end of file From e77914a344cf48e782cae0d9d92fd8a76c3f40df Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:06:05 +0100 Subject: [PATCH 2/2] docs: updated demo quickstart part --- README.md | 4 +-- docs/getting-started/quickstart.md | 52 +++++++++++++----------------- examples/README.md | 2 +- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e3369367..9d1745f4 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Install-Module IdLE Run the end-to-end demo (Plan → Execute): ```powershell -pwsh -File .\examples\run-demo.ps1 +pwsh -File .\examples\Invoke-IdleDemo.ps1 ``` The demo shows: @@ -96,7 +96,7 @@ Next steps: - Documentation entry point: `docs/index.md` - Workflow samples: `examples/workflows/` -- Repository demo: `examples/run-demo.ps1` +- Repository demo: `examples/Invoke-IdleDemo.ps1` - Pester tests: `tests/` --- diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 18a6a997..6dfb023d 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -8,54 +8,46 @@ This quickstart walks through the IdLE flow: ## Run the repository demo -From the repository root: +The repository includes a demo runner that showcases the full IdLE flow using predefined example workflows. ```powershell -pwsh -File .\examples\run-demo.ps1 +.\examples\Invoke-IdleDemo.ps1 ``` -## Minimal plan and execute +You can also list and run specific examples: ```powershell -$workflowPath = Join-Path (Get-Location) 'examples\workflows\joiner-with-when.psd1' - -$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user' -$plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request - -$providers = @{} -$result = Invoke-IdlePlan -Plan $plan -Providers $providers +.\examples\Invoke-IdleDemo.ps1 -List +.\examples\Invoke-IdleDemo.ps1 -Example ``` -## Inspect results and events - -Execution returns a result object containing step results and buffered events: +...or simply run all ```powershell -$result.Status -$result.Steps -$result.Events | Select-Object Type, StepName, Message +.\examples\Invoke-IdleDemo.ps1 -All ``` -## Optional: stream events with -EventSink - -If a host wants live progress, it can provide an **object** event sink. -The sink must implement `WriteEvent(event)`. +## The manual example runs -> Security note: ScriptBlock sinks are not supported. - -Example: +For understanding how IdLE is used programmatically, you can execute a workflow manually without the demo runner. ```powershell -$streamed = [System.Collections.Generic.List[object]]::new() +$workflowPath = Join-Path (Get-Location) 'examples\workflows\' +$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' +$plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request -$sink = [pscustomobject]@{} -$null = Add-Member -InputObject $sink -MemberType ScriptMethod -Name 'WriteEvent' -Value { - param($e) - [void]$streamed.Add($e) - Write-Host ("[{0}] {1}" -f $e.Type, $e.Message) +$providers = @{ + Identity = New-IdleMockIdentityProvider } +$result = Invoke-IdlePlan -Plan $plan -Providers $providers +``` + +When executing plans programmatically, IdLE returns a result object containing step results and buffered events. -$result = Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sink +```powershell +$result.Status +$result.Steps +$result.Events | Select-Object Type, StepName, Message ``` ## Next steps diff --git a/examples/README.md b/examples/README.md index a33952b3..2932bff7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ This folder contains runnable examples for IdLE. From the repository root: ```powershell -pwsh -File .\examples\run-demo.ps1 +pwsh -File .\examples\Invoke-IdleDemo.ps1 ``` The demo: