completers/sbx_completer/sbx_completer.ps1
|
<# .SYNOPSIS Registers Docker Sandboxes (`sbx`) tab-completion through the installed `sbx` executable. .DESCRIPTION This script registers importer-safe native completers for `sbx` and `sbx.exe`. The generated completion script (`sbx completion powershell`) is resolved lazily at completion time, cached, and then invoked through the installed Docker Sandboxes CLI's own PowerShell completer, so subcommands, flags and sandbox names always match the installed `sbx` version. Run once per session, or dot-source it from your PowerShell profile. #> Set-StrictMode -Version Latest function Get-SbxCommandPath { $cachedPath = Get-Variable -Name SbxCommandPath -Scope Script -ErrorAction Ignore if ($null -ne $cachedPath) { return $cachedPath.Value } $sbxCommand = Get-Command -Name 'sbx', 'sbx.exe' -CommandType Application -ErrorAction Ignore | Select-Object -First 1 $script:SbxCommandPath = if ($null -ne $sbxCommand) { $sbxCommand.Source } else { $null } $script:SbxCommandPath } function Get-SbxGeneratedCompletionScript { # A completion callback must stay silent: diagnostics go to the verbose stream only. $sbxCommandPath = Get-SbxCommandPath if ([string]::IsNullOrWhiteSpace($sbxCommandPath)) { Write-Verbose 'Docker Sandboxes CLI (sbx) was not found in PATH.' return $null } try { $completionScript = $null | & $sbxCommandPath completion powershell 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' } | Out-String if ([string]::IsNullOrWhiteSpace($completionScript)) { Write-Verbose 'sbx returned an empty completion script.' return $null } return $completionScript } catch { Write-Verbose ("Failed to load sbx completion: {0}" -f $_.Exception.Message) $null } } function Get-SbxCompletionInvoker { $cachedInvoker = Get-Variable -Name SbxCompletionInvoker -Scope Script -ErrorAction Ignore if ($null -ne $cachedInvoker) { return $cachedInvoker.Value } # A failed discovery is cached too, so a machine without sbx pays for it once per session. if (Get-Variable -Name SbxCompletionUnavailable -Scope Script -ErrorAction Ignore) { return $null } $completionScript = Get-SbxGeneratedCompletionScript if ([string]::IsNullOrWhiteSpace($completionScript)) { $script:SbxCompletionUnavailable = $true return $null } # The upstream script registers only the bare 'sbx' name; registration is owned by this file. $completionScript = $completionScript -replace ( "(?m)^\s*Register-ArgumentCompleter\s+-CommandName\s+'sbx'\s+-ScriptBlock\s+\$\{__sbxCompleterBlock\}\s*\r?$" ), '' # With no suggestions and the default directive (file completion allowed, e.g. 'sbx cp <path>') # $Values is $null, and '$null | ForEach-Object' still runs once, so the block would build a # CompletionResult from a null name and leave an exception in $Error on every such Tab. $completionScript = $completionScript -replace ( '(?m)^(\s*)\$Values \| ForEach-Object \{\s*$' ), '$1$$Values | Where-Object { $$null -ne $$_ } | ForEach-Object {' # cobra's block dereferences properties of an empty pipeline when there are no suggestions; run it # outside this script's strict mode so those accesses stay silent. $completionInvokerSource = @" Set-StrictMode -Off $completionScript & `${__sbxCompleterBlock} @args "@ try { $script:SbxCompletionInvoker = [scriptblock]::Create($completionInvokerSource) return $script:SbxCompletionInvoker } catch { Write-Verbose ("Failed to prepare sbx completion: {0}" -f $_.Exception.Message) $script:SbxCompletionUnavailable = $true $null } } function Get-SbxFlagValueSlot { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) # Closed value sets that 'sbx <command> --help' documents but cobra does not register. # --skills and --pull exist only for local sandboxes, so they are dropped under --cloud. $valueSets = @{ 'run --on-timeout' = 'stop', 'restart', 'delete' 'run --platform' = 'linux/amd64', 'linux/arm64' 'run --skills' = 'off', 'readonly', 'readwrite' 'run --pull' = 'always', 'missing', 'never' 'create --on-timeout' = 'stop', 'restart', 'delete' 'create --platform' = 'linux/amd64', 'linux/arm64' 'create --skills' = 'off', 'readonly', 'readwrite' 'create --pull' = 'always', 'missing', 'never' 'move --to' = 'local', 'cloud' 'move --on-timeout' = 'stop', 'delete' } $localOnlyFlags = '--skills', '--pull' $elements = @($CommandAst.CommandElements | Select-Object -Skip 1) $preceding = @($elements | Where-Object { $_.Extent.EndOffset -lt $CursorPosition } | ForEach-Object { $_.Extent.Text }) $current = $elements | Where-Object { $_.Extent.StartOffset -lt $CursorPosition -and $_.Extent.EndOffset -ge $CursorPosition } | Select-Object -First 1 $typed = if ($null -ne $current) { $current.Extent.Text.Substring(0, $CursorPosition - $current.Extent.StartOffset) } else { '' } # Everything after a bare '--' belongs to the agent; root flags take no value, so the first bare word is the command. if ($preceding -ccontains '--') { return $null } $command = $preceding | Where-Object { -not $_.StartsWith('-') } | Select-Object -First 1 if ($null -eq $command) { return $null } $attached = $typed -cmatch '^(--[a-z-]+=)(.*)$' if ($attached) { $flag = $Matches[1].TrimEnd('=') $prefix = $Matches[1] $valueText = $Matches[2] } elseif ($preceding.Count -gt 0 -and -not $typed.StartsWith('-')) { $flag = $preceding[-1] $prefix = '' $valueText = $typed } else { return $null } $values = $valueSets["$command $flag"] if ($null -eq $values -or (($preceding -ccontains '--cloud') -and $localOnlyFlags -ccontains $flag)) { return $null } $quote = if ($valueText.Length -gt 0 -and $valueText[0] -in "'", '"') { [string]$valueText[0] } else { '' } [pscustomobject]@{ Attached = $attached Prefix = $prefix Quote = $quote Value = $valueText.Substring($quote.Length) Values = $values } } function Get-SbxFlagValueCompletion { param([psobject]$Slot) foreach ($value in $Slot.Values) { if ($value.StartsWith($Slot.Value, [System.StringComparison]::OrdinalIgnoreCase)) { $completionText = '{0}{1}{2}{1}' -f $Slot.Prefix, $Slot.Quote, $value [System.Management.Automation.CompletionResult]::new($completionText, $value, 'ParameterValue', $value) } } } function Invoke-SbxCompletion { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) $flagValueSlot = Get-SbxFlagValueSlot -CommandAst $commandAst -CursorPosition $cursorPosition # cobra has no values for an attached '--flag=' here, and its block faults on that empty answer. if ($null -ne $flagValueSlot -and $flagValueSlot.Attached) { return Get-SbxFlagValueCompletion -Slot $flagValueSlot } $results = @() $completionInvoker = Get-SbxCompletionInvoker if ($null -ne $completionInvoker) { # cobra's block lets the child process's 'Completion ended with directive' banner reach stderr; # redirecting the whole invocation keeps the console clean. Its "" sentinel # (ShellCompDirectiveNoFileComp) is dropped: PowerShell rejects an empty completion text. try { $results = @(& $completionInvoker $wordToComplete $commandAst $cursorPosition 2>$null | Where-Object { -not ($_ -is [string] -and $_.Length -eq 0) }) } catch { Write-Verbose ("sbx completion block failed: {0}" -f $_.Exception.Message) } } if ($results.Count -eq 0 -and $null -ne $flagValueSlot) { return Get-SbxFlagValueCompletion -Slot $flagValueSlot } $results } Register-ArgumentCompleter -Native -CommandName @('sbx', 'sbx.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Invoke-SbxCompletion -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |