src/Contract.ps1
|
function New-QuietContract { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','',Justification='Constructs fresh in-memory contract state.')] [CmdletBinding(DefaultParameterSetName='Document')]param( [Parameter(Mandatory)][string]$Id,[string]$FilePath,[string[]]$ArgumentList=@(), [Parameter(Mandatory,ParameterSetName='Task')][scriptblock]$Execute, [Parameter(Mandatory,ParameterSetName='Document')][scriptblock]$Parse, [Parameter(Mandatory,ParameterSetName='Streaming')][scriptblock]$OnOutput, [Parameter(Mandatory,ParameterSetName='Batch')][scriptblock]$OnBatch, [object]$State,[scriptblock]$Complete,[scriptblock]$Projection, [string]$Summary='Contract completed.',[char]$RecordDelimiter=[char]10,[char]$StdErrDelimiter=[char]10, [string]$Encoding='utf-8',[Collections.IDictionary]$Environment=@{},[int[]]$SuccessExitCodes=@(0), [string[]]$Documentation=@(),[string[]]$SourcePath=@(),[string[]]$SupportedForms=@(),[string]$ObservedVersion) [pscustomobject]@{PSTypeName='QuietCli.Contract';Execute=$Execute;Id=$Id;FilePath=$FilePath;ArgumentList=@($ArgumentList);Parse=$Parse;OnOutput=$OnOutput;OnBatch=$OnBatch State=$State;Complete=$Complete;Projection=$Projection;Summary=$Summary;RecordDelimiter=$RecordDelimiter;StdErrDelimiter=$StdErrDelimiter Encoding=$Encoding;Environment=$Environment;SuccessExitCodes=$SuccessExitCodes;Documentation=$Documentation;SourcePath=$SourcePath SupportedForms=$SupportedForms;ObservedVersion=$ObservedVersion} } function Register-QuietCommand { [CmdletBinding()]param([Parameter(Mandatory)][string]$Command,[Parameter(Mandatory)][scriptblock]$Factory,[string]$ModuleId='session') $key=(Get-QuietExecutableName $Command) if($script:QuietCommands.ContainsKey($key)){ if($script:QuietCommands[$key].ModuleId -eq $ModuleId){$script:QuietCommands[$key].Factory=$Factory;return} throw "QuietCli command conflict: $key is already provided by $($script:QuietCommands[$key].ModuleId)." } $script:QuietCommands[$key]=@{Factory=$Factory;ModuleId=$ModuleId} } function Resolve-QuietCommand { [CmdletBinding()]param([Parameter(Mandatory)][string]$Command) $key=(Get-QuietExecutableName $Command) if(!$script:QuietCommands.ContainsKey($key)){ $modules=@((Get-QuietConfiguration).Modules|Where-Object Command -eq $key) if($modules.Count -gt 1){throw "Multiple configured modules for $key."} if($modules.Count){ $module=$modules[0] if((Get-QuietModuleHash $module.Path) -cne $module.Hash){throw 'Installed QuietCli module has changed; validate and install it again.'} Import-Module -Name $module.Path -Force -ErrorAction Stop } } if($script:QuietCommands.ContainsKey($key)){return $script:QuietCommands[$key]} } function Invoke-QuietContract { param($Contract,[hashtable]$Invocation,[hashtable]$Options=@{},[string]$ContractPath) if($Contract.PSObject.TypeNames -notcontains 'QuietCli.Contract'){throw 'A factory must return one QuietCli contract.'} $call=@{FilePath=$Contract.FilePath;ArgumentList=$Contract.ArgumentList;WorkingDirectory=$Invocation.WorkingDirectory;State=$Contract.State Summary=$Contract.Summary;Projection=$Contract.Projection;RecordDelimiter=$Contract.RecordDelimiter;StdErrDelimiter=$Contract.StdErrDelimiter Encoding=$Contract.Encoding;SuccessExitCodes=$Contract.SuccessExitCodes;Environment=$Contract.Environment} if($Contract.Parse){$call.Parse=$Contract.Parse}elseif($Contract.OnBatch){$call.OnBatch=$Contract.OnBatch}else{$call.OnOutput=$Contract.OnOutput} if($Contract.Complete){$call.Complete=$Contract.Complete} foreach($name in @('TimeoutSeconds','CancellationToken','CancellationFile','MaxCaptureBytes','MaxRecordCharacters','InputText')){if($Options.ContainsKey($name)){$call[$name]=$Options[$name]}} if($Contract.Execute){$result=Invoke-QuietTask -ScriptBlock $Contract.Execute -ArgumentList @($Invocation) -AsObject} else{$result=Invoke-QuietCli @call} $result.Internal.Origin=$Invocation;$result.Projection=$Contract.Projection;$result.Summary=$Contract.Summary foreach($source in $result.Internal.Sources.Values){ $source.Contract=$Contract.Id;$source.Documentation=$Contract.Documentation;if($Contract.ObservedVersion){$source.ObservedVersion=$Contract.ObservedVersion} $source.SourcePaths=@($Contract.SourcePath)+@($ContractPath|Where-Object {$_}) $source.OriginalRequest=$Invocation } if($Options.Record){Save-QuietRecord $result -Explicit|Out-Null} $result } function Invoke-QuietPump { [CmdletBinding()]param([Parameter(Mandatory)][string]$Command,[string[]]$ArgumentList=@(), [string]$ContractPath,[string]$WorkingDirectory=(Get-Location).Path,[switch]$Record,[switch]$AsObject,[switch]$AsJson,[switch]$AsYaml, [int]$MaxItems=20,[int]$MaxOutputBytes=4096,[int]$TimeoutSeconds=600,[long]$MaxCaptureBytes=-1, [int]$MaxRecordCharacters=65536,[int]$ExcerptLines=20, [Threading.CancellationToken]$CancellationToken=[Threading.CancellationToken]::None,[string]$CancellationFile, [AllowNull()][string]$InputText,[scriptblock]$OnOutput,[scriptblock]$OnBatch,[object]$State,[scriptblock]$Complete,[scriptblock]$Parse,[scriptblock]$Projection) if(@($AsObject,$AsJson,$AsYaml|Where-Object {$_}).Count -gt 1){throw 'AsObject, AsJson and AsYaml are exclusive.'} $settings=Get-QuietConfiguration if(!$PSBoundParameters.ContainsKey('MaxItems')){$MaxItems=$settings.MaxItems} if(!$PSBoundParameters.ContainsKey('MaxOutputBytes')){$MaxOutputBytes=$settings.MaxOutputBytes} if(!$PSBoundParameters.ContainsKey('TimeoutSeconds')){$TimeoutSeconds=$settings.TimeoutSeconds} $invocation=@{FilePath=$Command;ArgumentList=@($ArgumentList);WorkingDirectory=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($WorkingDirectory)} try{ if($OnOutput -or $OnBatch -or $Parse){ $direct=@{FilePath=$Command;ArgumentList=$ArgumentList;WorkingDirectory=$invocation.WorkingDirectory;State=$State TimeoutSeconds=$TimeoutSeconds;CancellationToken=$CancellationToken;MaxCaptureBytes=$MaxCaptureBytes;MaxRecordCharacters=$MaxRecordCharacters;Record=$Record} if($OnOutput){$direct.OnOutput=$OnOutput}elseif($OnBatch){$direct.OnBatch=$OnBatch}else{$direct.Parse=$Parse} if($Complete){$direct.Complete=$Complete};if($Projection){$direct.Projection=$Projection} if($PSBoundParameters.ContainsKey('InputText')){$direct.InputText=$InputText};if($CancellationFile){$direct.CancellationFile=$CancellationFile} $result=Invoke-QuietCli @direct }else{ $contract=$null if($ContractPath){$ContractPath=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ContractPath);$contract=Invoke-QuietFactory -Path $ContractPath -Invocation $invocation} else{$route=Resolve-QuietCommand $Command;if($route){$contract=Invoke-QuietFactory -Factory $route.Factory -Invocation $invocation}} if(!$contract){$result=New-QuietResult -Outcome Unsupported -Summary 'No applicable contract. Ask the agent to read the command documentation and provide a contract.' -ErrorInfo @{Code='ContractNotFound';Phase='Routing'} -Execution @{ExitCode=$null}} elseif($contract.PSObject.TypeNames -contains 'QuietCli.Result'){$result=$contract;Add-QuietTaskResult $result} else{$options=@{};foreach($key in $PSBoundParameters.Keys){$options[$key]=$PSBoundParameters[$key]};$result=Invoke-QuietContract $contract $invocation $options $ContractPath} } }catch{$result=New-QuietResult -Outcome Failed -Summary 'Contract invocation failed.' -ErrorInfo @{Code='ContractFailed';Phase='Routing';Message=$_.Exception.Message} -Execution @{ExitCode=$null}} if($AsObject){return $result} Out-QuietResult -InputObject $result -AsJson:$AsJson -AsYaml:$AsYaml -MaxItems $MaxItems -MaxOutputBytes $MaxOutputBytes -ExcerptLines $ExcerptLines } |