Private/Invoke-DFToolCompanion.ps1
|
#Requires -Version 7.0 function Invoke-DFToolCompanion { <# .SYNOPSIS Runs a tool's one-time setup step (setup.seed files and Tools/<name>.setup.ps1, if not yet recorded and not skipped), then dot-sources its companion Tools/<name>.ps1, setting $DFCurrentTool around each. .DESCRIPTION The regular companion runs on every load. The setup step runs first, at most once ever per tool -- see docs/superpowers/specs/2026-09-04-tool-setup-lifecycle-design.md -- and a setup script is responsible for calling Complete-DFToolSetup itself on success (a tool with only setup.seed is recorded here); a thrown error here is caught and warned so the next Register-DFTool call retries it. Dot-sourcing runs the companion directly in this function's own scope (not a child scope), so $DFCurrentTool set here immediately before each dot-source is what the companion sees -- the sidecar contract only requires "set immediately before, cleared immediately after," not that it happen inside Register-DFTool specifically. .PARAMETER Tool The tool record whose companion(s) to run. .PARAMETER ToolsPath The resolved Tools/ directory to look for companions in. .PARAMETER SkipSetup Tool names (the SkipSetup setting) whose one-time setup step (seeds and script) must never run. .PARAMETER WonRoles Roles this tool won: { Role; Hook; HookRequired }. After the companion runs, each role's hook function, if the companion itself defined it, is dot-sourced with -Tool and -Role. A throwing hook warns; a missing hook warns when HookRequired. Roles the tool lost are never passed, so their hooks never run. .PARAMETER SetupOnly Run only the setup step and return before the companion (Invoke-DFToolSetup). .OUTPUTS None #> # $DFCurrentTool is set before dot-sourcing companions so sidecars can # read it. PSScriptAnalyzer can't see the companion scope, so suppress # the false positive. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'DFCurrentTool')] [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Tool, [Parameter(Mandatory)] [string]$ToolsPath, [string[]]$SkipSetup = @(), [AllowEmptyCollection()] [object[]]$WonRoles = @(), [switch]$SetupOnly ) # The companion body and its hooks are dot-sourced into this scope, so they # can overwrite any local here ($Tool, $companion, ...). Everything this # function needs afterwards is captured first under one unlikely name. $__dfCall = @{ Tool = $Tool Name = $Tool.name Companion = Join-Path $ToolsPath "$($Tool.name).ps1" Setup = Join-Path $ToolsPath "$($Tool.name).setup.ps1" SkipSetup = @($SkipSetup) WonRoles = @($WonRoles) SetupOnly = [bool]$SetupOnly } $__dfCall.HasCompanion = Test-Path $__dfCall.Companion -PathType Leaf # The one-time setup step runs first, so the companion can rely on what it # created: setup.seed files, then Tools/<name>.setup.ps1, which records its # own completion. With no script, the seeds are recorded here. $__dfCall.HasSetupScript = Test-Path $__dfCall.Setup -PathType Leaf $__dfCall.HasSeed = [bool]$__dfCall.Tool.PSObject.Properties['setup']?.Value?.seed if (($__dfCall.HasSetupScript -or $__dfCall.HasSeed) -and $__dfCall.Name -notin $__dfCall.SkipSetup -and -not (Get-DFToolSetupState).PSObject.Properties[$__dfCall.Name]) { try { $__dfCall.Seeded = @(Invoke-DFToolSeed -Tool $__dfCall.Tool -ToolsPath $ToolsPath) if ($__dfCall.HasSetupScript) { $DFCurrentTool = $__dfCall.Tool . ($__dfCall.Setup) } else { Complete-DFToolSetup -Name $__dfCall.Name -Actions $__dfCall.Seeded } } catch { Write-Warning "DotForge: $($__dfCall.Name) one-time setup failed: $($_.Exception.Message)" } Remove-Variable -Name DFCurrentTool -ErrorAction Ignore } if ($__dfCall.SetupOnly) { return } if ($__dfCall.HasCompanion) { $DFCurrentTool = $__dfCall.Tool . ($__dfCall.Companion) } # Role hooks: plain functions the companion defined in this scope. Only a # definition from this tool's own companion file counts, so a stray global # of the same name is never mistaken for the hook. Dot-sourced so init # scripts run in the same scope the companion body did. foreach ($__dfWon in $__dfCall.WonRoles) { $__dfCall.Won = $__dfWon $__dfHook = if ($__dfCall.HasCompanion) { Get-Item -LiteralPath "function:$($__dfWon.Hook)" -ErrorAction Ignore } if ($__dfHook -and $__dfHook.ScriptBlock.File -and [System.IO.Path]::GetFullPath($__dfHook.ScriptBlock.File) -eq [System.IO.Path]::GetFullPath($__dfCall.Companion)) { $DFCurrentTool = $__dfCall.Tool try { . $__dfHook.ScriptBlock -Tool $__dfCall.Tool -Role $__dfCall.Won.Role } catch { Write-Warning "DotForge: $($__dfCall.Name) failed to activate as the $($__dfCall.Won.Role) tool: $($_.Exception.Message)" } } elseif ($__dfCall.Won.HookRequired) { Write-Warning "DotForge: $($__dfCall.Name) declares the $($__dfCall.Won.Role) role but its companion defines no $($__dfCall.Won.Hook) — role not activated." } } Remove-Variable -Name DFCurrentTool -ErrorAction Ignore } |