completers/comfy_cli_completer/comfy_cli_completer.ps1

# Help-driven completion for the installed comfy-cli executable.
Set-StrictMode -Version 2.0

function Get-ComfyCliHelpText {
    param([string[]]$Segments)

    $exe = Get-Command comfy-cli.exe -CommandType Application -ErrorAction Ignore | Select-Object -First 1
    if ($null -eq $exe) { return '' }

    $process = [System.Diagnostics.Process]::new()
    try {
        $process.StartInfo = [System.Diagnostics.ProcessStartInfo]::new($exe.Source)
        $process.StartInfo.UseShellExecute = $false
        $process.StartInfo.CreateNoWindow = $true
        $process.StartInfo.RedirectStandardInput = $true
        $process.StartInfo.RedirectStandardOutput = $true
        $process.StartInfo.RedirectStandardError = $true
        $process.StartInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8
        $process.StartInfo.StandardErrorEncoding = [System.Text.Encoding]::UTF8
        $process.StartInfo.Environment['PYTHONIOENCODING'] = 'utf-8'
        $process.StartInfo.Environment['NO_COLOR'] = '1'
        foreach ($segment in @($Segments)) { [void]$process.StartInfo.ArgumentList.Add($segment) }
        [void]$process.StartInfo.ArgumentList.Add('--help')
        [void]$process.Start()
        $process.StandardInput.Close()
        $stdout = $process.StandardOutput.ReadToEndAsync()
        $stderr = $process.StandardError.ReadToEndAsync()
        if (-not $process.WaitForExit(5000)) {
            $process.Kill($true)
            return ''
        }
        return ($stdout.GetAwaiter().GetResult() + "`n" + $stderr.GetAwaiter().GetResult()) -replace '\e\[[0-9;?]*[ -/]*[@-~]', ''
    } catch {
        return ''
    } finally {
        $process.Dispose()
    }
}

function ConvertFrom-ComfyCliHelp {
    param([string]$HelpText)

    $commands = [System.Collections.Hashtable]::new([System.StringComparer]::Ordinal)
    $options = [System.Collections.Hashtable]::new([System.StringComparer]::Ordinal)
    $usage = ''
    $section = ''
    $current = $null
    foreach ($line in ($HelpText -split '\r?\n')) {
        if ($line -match '^\s*Usage:\s*comfy-cli(?:\.exe)?\s*(.*)$') { $usage = $Matches[1].Trim(); continue }
        if ($line -match '^\s*┌─\s*(Options|Commands)\b') { $section = $Matches[1]; $current = $null; continue }
        if ($line -match '^\s*└') { $section = ''; $current = $null; continue }
        if ($section -eq '' -or $line -notmatch '^\s*│(?<content>.*?)│\s*$') { continue }
        $raw = $Matches.content
        $content = $raw.Trim()
        if (-not $content) { continue }

        if ($section -eq 'Commands') {
            if ($content -match '^(?<name>[A-Za-z0-9][A-Za-z0-9-]*)\s{2,}(?<description>\S.*)$') {
                $commands[$Matches.name] = $Matches.description.Trim()
                $current = $Matches.name
            } elseif ($null -ne $current) {
                $commands[$current] = ($commands[$current] + ' ' + $content).Trim()
            }
            continue
        }

        $match = if ($raw -match '^\s{0,3}--?') {
            [regex]::Match($content, '^(?:(?<name>--?[A-Za-z][A-Za-z0-9-]*)(?:\s+|,\s*)?)+')
        } else { [System.Text.RegularExpressions.Match]::Empty }
        if ($match.Success) {
            $names = @($match.Groups['name'].Captures | ForEach-Object { $_.Value })
            $remainder = $content.Substring($match.Length).Trim()
            $meta = ''
            if ($remainder -match '^<(?<meta>[^>]+)>\s*(?<description>.*)$') {
                $meta = $Matches.meta
                $remainder = $Matches.description.Trim()
            } elseif ($remainder -match '^(?<meta>[A-Z][A-Z0-9_]*(?:=[A-Z][A-Z0-9_]*)?)\s{2,}(?<description>.+)$') {
                $meta = $Matches.meta
                $remainder = $Matches.description.Trim()
            }
            $entry = [pscustomobject]@{ Names = $names; Meta = $meta; Description = $remainder }
            foreach ($name in $names) {
                if ($name -in @('--install-completion', '--show-completion')) { continue }
                $options[$name] = $entry
            }
            $current = $entry
        } elseif ($null -ne $current) {
            $current.Description = ($current.Description + ' ' + $content).Trim()
        }
    }

    [pscustomobject]@{ Commands = $commands; Options = $options; Usage = $usage }
}

function Get-ComfyCliCatalog {
    param([string[]]$Segments)

    if (-not (Get-Variable -Name ComfyCliCatalogs -Scope Script -ErrorAction Ignore)) {
        $script:ComfyCliCatalogs = @{}
    }
    $key = @($Segments) -join ' '
    if ($script:ComfyCliCatalogs.ContainsKey($key)) { return $script:ComfyCliCatalogs[$key] }

    $catalog = ConvertFrom-ComfyCliHelp (Get-ComfyCliHelpText $Segments)
    if ($catalog.Options.Count -gt 0 -or $catalog.Commands.Count -gt 0) {
        $script:ComfyCliCatalogs[$key] = $catalog
    }
    return $catalog
}

function New-ComfyCliCompletion {
    param([string]$Text, [string]$Description, [string]$Type = 'ParameterValue', [string]$ListText = '')

    if (-not $Text) { return }
    if (-not $ListText) { $ListText = $Text }
    if (-not $Description) { $Description = $ListText }
    $insert = $Text
    $attached = [regex]::Match($Text, '^(?<option>--?[A-Za-z][A-Za-z0-9-]*=)(?<value>.*)$')
    if ($attached.Success -and $attached.Groups['value'].Value -match '[\s''"`$;|&(){}<>#@]') {
        $insert = $attached.Groups['option'].Value + "'" + $attached.Groups['value'].Value.Replace("'", "''") + "'"
    } elseif ($Text -match '[\s''"`$;|&(){}<>#@]') {
        $insert = "'" + $Text.Replace("'", "''") + "'"
    }
    [System.Management.Automation.CompletionResult]::new($insert, $ListText, $Type, $Description)
}

function Get-ComfyCliPathCompletion {
    param([string]$Word, [string]$Prefix = '', [bool]$DirectoryOnly = $false)

    $word = $Word.Trim("'", '"')
    if ($word -match '^(?:\\\\|//)' -or $word -match '^[^:]+::') {
        New-ComfyCliCompletion ($Prefix + $(if ($word) { $word } else { '<path>' })) 'Enter a local path; remote paths are not enumerated'
        return
    }
    $parent = '.'
    $leaf = $word
    if ($word -match '[/\\]$') { $parent = $word; $leaf = '' }
    elseif ($word -match '[/\\]') { $parent = Split-Path -Path $word -Parent; $leaf = Split-Path -Path $word -Leaf }
    $found = $false
    if (Test-Path -LiteralPath $parent -PathType Container) {
        foreach ($item in (Get-ChildItem -LiteralPath $parent -Force -ErrorAction Ignore)) {
            if ($DirectoryOnly -and -not $item.PSIsContainer) { continue }
            if (-not $item.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
            $candidate = if ($parent -eq '.') { $item.Name } else { Join-Path $parent $item.Name }
            if ($item.PSIsContainer) { $candidate += [System.IO.Path]::DirectorySeparatorChar }
            New-ComfyCliCompletion ($Prefix + $candidate) $item.FullName
            $found = $true
        }
    }
    if (-not $found) {
        New-ComfyCliCompletion ($Prefix + $(if ($word) { $word } else { '<path>' })) 'Enter a local path'
    }
}

function Get-ComfyCliValueCompletion {
    param([string]$Option, [object]$Entry, [string]$Word, [string]$Prefix = '')

    $value = $Word.Trim("'", '"')
    $name = $Option.TrimStart('-')
    if ($name -match '(?i)(file|folder|directory|path|workspace|workflow|output|input|config|image)$' -or $Entry.Meta -match '(?i)^(file|path|dir|directory)$') {
        Get-ComfyCliPathCompletion $value $Prefix ($name -match '(?i)(folder|directory|workspace)$')
        return
    }

    $description = $Entry.Description
    if ($Option -ceq '--where') {
        $root = Get-ComfyCliCatalog -Segments @()
        if ($root.Options.ContainsKey('--where')) { $description = $root.Options['--where'].Description }
    }
    $choices = @()
    if ($description -match '(?i)\b(?:choices?|one of):?\s*\[?([A-Za-z][A-Za-z0-9_-]*(?:\s*[,|]\s*[A-Za-z][A-Za-z0-9_-]*)+)') {
        $choices = @($Matches[1] -split '\s*[,|]\s*')
    } elseif ($description -match "'([A-Za-z][A-Za-z0-9_-]*)'\s+or\s+'([A-Za-z][A-Za-z0-9_-]*)'") {
        $choices = @($Matches[1], $Matches[2])
    }
    if ($choices.Count -gt 0) {
        foreach ($choice in $choices) {
            if ($choice.StartsWith($value, [System.StringComparison]::OrdinalIgnoreCase)) {
                New-ComfyCliCompletion ($Prefix + $choice) $Entry.Description
            }
        }
        return
    }
    $hint = if ($Entry.Meta) { '<' + $Entry.Meta + '>' } else { '<value>' }
    New-ComfyCliCompletion ($Prefix + $(if ($value) { $value } else { $hint })) "Enter $hint for $Option"
}

function Get-ComfyCliOperandCompletion {
    param([object]$Catalog, [int]$Index, [string]$Word)

    $usage = $Catalog.Usage -replace '^\s*(?:\S+\s+)*?\[OPTIONS\]\s*', ''
    $parts = @($usage -split '\s+' | Where-Object { $_ -and $_ -notin @('COMMAND', '[ARGS]...', '[OPTIONS]') })
    if ($parts.Count -eq 0) { return }
    $operand = if ($Index -lt $parts.Count) { $parts[$Index] } elseif ($parts.Count -gt 0 -and $parts[-1] -match '\.\.\.$') { $parts[-1] } else { '<argument>' }
    $operand = $operand.Trim('[', ']', '{', '}', '.', '<', '>')
    if (-not $operand) { $operand = 'argument' }
    if ($operand -match '(?i)(file|files|path|workflow|image|folder|directory|filenames)') {
        Get-ComfyCliPathCompletion $Word
    } else {
        New-ComfyCliCompletion $(if ($Word) { $Word } else { '<' + $operand + '>' }) "Enter $operand"
    }
}

function Complete-ComfyCli {
    param([string]$Word, [System.Management.Automation.Language.CommandAst]$Ast, [int]$Cursor)

    $tokens = @()
    foreach ($element in @($Ast.CommandElements | Select-Object -Skip 1)) {
        if ($element.Extent.StartOffset -ge $Cursor) { break }
        if ($element.Extent.EndOffset -ge $Cursor -and $Word) { break }
        $text = $element.Extent.Text
        if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $text = $element.Value }
        $tokens += $text
    }

    $segments = @()
    $catalog = Get-ComfyCliCatalog $segments
    $operands = @()
    $pending = $null
    $endOptions = $false
    foreach ($token in $tokens) {
        if ($null -ne $pending) { $pending = $null; continue }
        if ($token -ceq '--') { $endOptions = $true; continue }
        if (-not $endOptions -and $token.StartsWith('-')) {
            $optionName = ($token -split '=', 2)[0]
            if ($token -notmatch '=' -and $catalog.Options.ContainsKey($optionName) -and $catalog.Options[$optionName].Meta) {
                $pending = $optionName
            }
            continue
        }
        if (-not $endOptions -and $operands.Count -eq 0 -and $catalog.Commands.ContainsKey($token)) {
            $segments += $token
            $catalog = Get-ComfyCliCatalog $segments
            continue
        }
        $operands += $token
    }

    if ($null -ne $pending) {
        Get-ComfyCliValueCompletion $pending $catalog.Options[$pending] $Word
        return
    }
    if (-not $endOptions -and $Word -match '^(?<option>--?[A-Za-z][A-Za-z0-9-]*)=(?<value>.*)$') {
        $option = $Matches.option
        $value = $Matches.value
        if ($catalog.Options.ContainsKey($option) -and $catalog.Options[$option].Meta) {
            Get-ComfyCliValueCompletion $option $catalog.Options[$option] $value ($option + '=')
            return
        }
    }

    $found = $false
    if (-not $endOptions -and ($Word.StartsWith('-') -or -not $Word)) {
        foreach ($option in @($catalog.Options.Keys | Sort-Object -CaseSensitive)) {
            if ($option.StartsWith($Word, [System.StringComparison]::Ordinal)) {
                New-ComfyCliCompletion $option $catalog.Options[$option].Description 'ParameterName'
                $found = $true
            }
        }
    }
    if (-not $endOptions -and $operands.Count -eq 0 -and -not $Word.StartsWith('-')) {
        foreach ($command in @($catalog.Commands.Keys | Sort-Object)) {
            if ($command.StartsWith($Word, [System.StringComparison]::OrdinalIgnoreCase)) {
                New-ComfyCliCompletion $command $catalog.Commands[$command]
                $found = $true
            }
        }
    }
    if ($found -and ($Word.StartsWith('-') -or $catalog.Commands.Count -gt 0)) { return }
    if ($Word.StartsWith('-') -and -not $endOptions) { return }
    Get-ComfyCliOperandCompletion $catalog $operands.Count $Word
}

Register-ArgumentCompleter -Native -CommandName @('comfy-cli', 'comfy-cli.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
    Complete-ComfyCli -Word $wordToComplete -Ast $commandAst -Cursor $cursorPosition
}