completers/rtk_completer/rtk_completer.ps1

Set-StrictMode -Version 2.0

if (-not (Get-Variable -Name RtkCompletionCache -Scope Script -ErrorAction Ignore)) {
    $script:RtkCompletionCache = @{
        ExecutableResolved = $false
        Executable         = $null
        Nodes              = @{}
    }
}

function New-RtkCompletionResult {
    param(
        [Parameter(Mandatory)]
        [string]$CompletionText,

        [string]$ToolTip,

        [string]$ResultType = 'ParameterValue',

        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function Test-RtkStartsWith {
    param(
        [string]$Candidate,
        [string]$Prefix
    )

    [string]::IsNullOrEmpty($Prefix) -or $Candidate.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase)
}

function New-RtkOptionSpec {
    param(
        [Parameter(Mandatory)]
        [string[]]$Names,

        [string]$Description,

        [string]$ValueKind = 'None',

        [string]$Metavar = '',

        [string[]]$Values = @()
    )

    [pscustomobject]@{
        Names       = $Names
        Description = $Description
        ValueKind   = $ValueKind
        Metavar     = $Metavar
        Values      = @($Values)
    }
}

function New-RtkCommandSpec {
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [string]$Description
    )

    [pscustomobject]@{
        Name        = $Name
        Description = $Description
    }
}

function ConvertTo-RtkArgumentSpec {
    param(
        [Parameter(Mandatory)]
        [string]$Metavar,

        [string]$Description,

        [string]$ValueKind = 'Text',

        [string[]]$Values = @(),

        [bool]$Variadic = $false
    )

    [pscustomobject]@{
        Metavar     = $Metavar
        Description = $Description
        ValueKind   = $ValueKind
        Values      = @($Values)
        Variadic    = $Variadic
    }
}

function ConvertTo-RtkNode {
    param(
        [object[]]$Commands = @(),
        [object[]]$Options = @(),
        [object[]]$Arguments = @(),
        [string]$Source = 'static'
    )

    [pscustomobject]@{
        Commands  = @($Commands)
        Options   = @($Options)
        Arguments = @($Arguments)
        Source    = $Source
    }
}

function Get-RtkRootCommands {
    @(
        New-RtkCommandSpec -Name 'ls' -Description 'List directory contents with token-optimized output'
        New-RtkCommandSpec -Name 'tree' -Description 'Directory tree with token-optimized output'
        New-RtkCommandSpec -Name 'read' -Description 'Read file with intelligent filtering'
        New-RtkCommandSpec -Name 'smart' -Description 'Generate 2-line technical summary'
        New-RtkCommandSpec -Name 'git' -Description 'Git commands with compact output'
        New-RtkCommandSpec -Name 'gh' -Description 'GitHub CLI (gh) commands with token-optimized output'
        New-RtkCommandSpec -Name 'glab' -Description 'GitLab CLI (glab) commands with token-optimized output'
        New-RtkCommandSpec -Name 'aws' -Description 'AWS CLI with compact output'
        New-RtkCommandSpec -Name 'psql' -Description 'PostgreSQL client with compact output'
        New-RtkCommandSpec -Name 'pnpm' -Description 'pnpm commands with ultra-compact output'
        New-RtkCommandSpec -Name 'err' -Description 'Run command and show only errors/warnings'
        New-RtkCommandSpec -Name 'test' -Description 'Run tests and show only failures'
        New-RtkCommandSpec -Name 'json' -Description 'Show JSON (compact values by default)'
        New-RtkCommandSpec -Name 'deps' -Description 'Summarize project dependencies'
        New-RtkCommandSpec -Name 'env' -Description 'Show environment variables (filtered)'
        New-RtkCommandSpec -Name 'find' -Description 'Find files with compact tree output'
        New-RtkCommandSpec -Name 'diff' -Description 'Ultra-condensed diff (only changed lines)'
        New-RtkCommandSpec -Name 'log' -Description 'Filter and deduplicate log output'
        New-RtkCommandSpec -Name 'dotnet' -Description '.NET commands with compact output'
        New-RtkCommandSpec -Name 'docker' -Description 'Docker commands with compact output'
        New-RtkCommandSpec -Name 'kubectl' -Description 'Kubectl commands with compact output'
        New-RtkCommandSpec -Name 'oc' -Description 'OpenShift CLI (oc) commands with compact output'
        New-RtkCommandSpec -Name 'summary' -Description 'Run command and show heuristic summary'
        New-RtkCommandSpec -Name 'grep' -Description 'Compact grep - strips whitespace, truncates, groups by file'
        New-RtkCommandSpec -Name 'rg' -Description 'Compact ripgrep - runs rg natively'
        New-RtkCommandSpec -Name 'init' -Description 'Initialize rtk instructions for assistant CLI usage'
        New-RtkCommandSpec -Name 'wget' -Description 'Download with compact output'
        New-RtkCommandSpec -Name 'wc' -Description 'Word/line/byte count with compact output'
        New-RtkCommandSpec -Name 'gain' -Description 'Show token savings summary and history'
        New-RtkCommandSpec -Name 'cc-economics' -Description 'Claude Code economics analysis'
        New-RtkCommandSpec -Name 'config' -Description 'Show or create configuration file'
        New-RtkCommandSpec -Name 'jest' -Description 'Jest commands with compact output'
        New-RtkCommandSpec -Name 'vitest' -Description 'Vitest commands with compact output'
        New-RtkCommandSpec -Name 'ctest' -Description 'CTest with compact output'
        New-RtkCommandSpec -Name 'prisma' -Description 'Prisma commands with compact output'
        New-RtkCommandSpec -Name 'tsc' -Description 'TypeScript compiler with grouped error output'
        New-RtkCommandSpec -Name 'next' -Description 'Next.js build with compact output'
        New-RtkCommandSpec -Name 'lint' -Description 'ESLint with grouped rule violations'
        New-RtkCommandSpec -Name 'prettier' -Description 'Prettier format checker with compact output'
        New-RtkCommandSpec -Name 'format' -Description 'Universal format checker'
        New-RtkCommandSpec -Name 'playwright' -Description 'Playwright E2E tests with compact output'
        New-RtkCommandSpec -Name 'cargo' -Description 'Cargo commands with compact output'
        New-RtkCommandSpec -Name 'npm' -Description 'npm run with filtered output'
        New-RtkCommandSpec -Name 'npx' -Description 'npx with intelligent routing'
        New-RtkCommandSpec -Name 'bun' -Description 'Bun runtime commands with compact output'
        New-RtkCommandSpec -Name 'bunx' -Description 'bunx with passthrough + auto-filter'
        New-RtkCommandSpec -Name 'curl' -Description 'Curl with auto-JSON detection and schema output'
        New-RtkCommandSpec -Name 'discover' -Description 'Discover missed RTK savings from Claude Code history'
        New-RtkCommandSpec -Name 'session' -Description 'Show RTK adoption across Claude Code sessions'
        New-RtkCommandSpec -Name 'telemetry' -Description 'Manage telemetry consent and data'
        New-RtkCommandSpec -Name 'learn' -Description 'Learn CLI corrections from Claude Code error history'
        New-RtkCommandSpec -Name 'run' -Description 'Execute a shell command via sh -c'
        New-RtkCommandSpec -Name 'proxy' -Description 'Execute command without filtering but track usage'
        New-RtkCommandSpec -Name 'pipe' -Description 'Read stdin, apply filter, print filtered output'
        New-RtkCommandSpec -Name 'trust' -Description 'Trust project-local TOML filters in current directory'
        New-RtkCommandSpec -Name 'untrust' -Description 'Revoke trust for project-local TOML filters'
        New-RtkCommandSpec -Name 'verify' -Description 'Verify hook integrity and run TOML filter inline tests'
        New-RtkCommandSpec -Name 'ruff' -Description 'Ruff linter/formatter with compact output'
        New-RtkCommandSpec -Name 'pytest' -Description 'Pytest test runner with compact output'
        New-RtkCommandSpec -Name 'mypy' -Description 'Mypy type checker with grouped error output'
        New-RtkCommandSpec -Name 'php' -Description 'PHP command runner with compact output'
        New-RtkCommandSpec -Name 'phpunit' -Description 'PHPUnit test runner with compact output'
        New-RtkCommandSpec -Name 'phpstan' -Description 'PHPStan analyzer with compact output'
        New-RtkCommandSpec -Name 'pest' -Description 'Pest test runner with compact output'
        New-RtkCommandSpec -Name 'paratest' -Description 'ParaTest parallel test runner with compact output'
        New-RtkCommandSpec -Name 'ecs' -Description 'EasyCodingStandard (ECS) code style fixer with compact output'
        New-RtkCommandSpec -Name 'pint' -Description 'Laravel Pint code style fixer with compact output'
        New-RtkCommandSpec -Name 'phpt' -Description 'PHP run-tests.php (.phpt) with compact summary'
        New-RtkCommandSpec -Name 'rake' -Description 'Rake/Rails test with compact Minitest output'
        New-RtkCommandSpec -Name 'rubocop' -Description 'RuboCop linter with compact output'
        New-RtkCommandSpec -Name 'rspec' -Description 'RSpec test runner with compact output'
        New-RtkCommandSpec -Name 'pip' -Description 'Pip package manager with compact output'
        New-RtkCommandSpec -Name 'uv' -Description 'uv run with compact output'
        New-RtkCommandSpec -Name 'deno' -Description 'Deno runtime commands with compact output'
        New-RtkCommandSpec -Name 'go' -Description 'Go commands with compact output'
        New-RtkCommandSpec -Name 'sbt' -Description 'SBT (Scala Build Tool) commands with compact output'
        New-RtkCommandSpec -Name 'gt' -Description 'Graphite (gt) stacked PR commands with compact output'
        New-RtkCommandSpec -Name 'golangci-lint' -Description 'golangci-lint wrapper with compact run support'
        New-RtkCommandSpec -Name 'gradlew' -Description 'Android Gradle wrapper with compact output'
        New-RtkCommandSpec -Name 'mvn' -Description 'Apache Maven wrapper with compact output'
        New-RtkCommandSpec -Name 'mvnd' -Description 'Maven Daemon (mvnd) with compact output'
        New-RtkCommandSpec -Name 'hook-audit' -Description 'Show hook rewrite audit metrics'
        New-RtkCommandSpec -Name 'rewrite' -Description 'Rewrite a raw command to its RTK equivalent'
        New-RtkCommandSpec -Name 'hook' -Description 'Hook processors for LLM CLI tools'
        New-RtkCommandSpec -Name 'help' -Description 'Print this message or the help of the given subcommand(s)'
    )
}

function Get-RtkGlobalOptions {
    @(
        New-RtkOptionSpec -Names @('-v', '--verbose') -Description 'Verbosity level (-v, -vv, -vvv)'
        New-RtkOptionSpec -Names @('--ultra-compact') -Description 'Ultra-compact mode: ASCII icons, inline format'
        New-RtkOptionSpec -Names @('--skip-env') -Description 'Set SKIP_ENV_VALIDATION=1 for child processes'
        New-RtkOptionSpec -Names @('-h', '--help') -Description 'Print help'
        New-RtkOptionSpec -Names @('-V', '--version') -Description 'Print version'
    )
}

function Get-RtkCommonOptionTable {
    @(
        New-RtkOptionSpec -Names @('--ultra-compact') -Description 'Ultra-compact mode: ASCII icons, inline format'
        New-RtkOptionSpec -Names @('--skip-env') -Description 'Set SKIP_ENV_VALIDATION=1 for child processes'
        New-RtkOptionSpec -Names @('-h', '--help') -Description 'Print help'
    )
}

function Get-RtkGitCommands {
    @(
        New-RtkCommandSpec -Name 'diff' -Description 'Condensed diff output'
        New-RtkCommandSpec -Name 'log' -Description 'One-line commit history'
        New-RtkCommandSpec -Name 'status' -Description 'Compact status'
        New-RtkCommandSpec -Name 'show' -Description 'Compact show'
        New-RtkCommandSpec -Name 'add' -Description 'Add files'
        New-RtkCommandSpec -Name 'commit' -Description 'Commit'
        New-RtkCommandSpec -Name 'checkout' -Description 'Checkout branch or restore paths'
        New-RtkCommandSpec -Name 'push' -Description 'Push'
        New-RtkCommandSpec -Name 'pull' -Description 'Pull'
        New-RtkCommandSpec -Name 'branch' -Description 'Compact branch listing'
        New-RtkCommandSpec -Name 'fetch' -Description 'Fetch'
        New-RtkCommandSpec -Name 'stash' -Description 'Stash management (list, show, pop, apply, drop)'
        New-RtkCommandSpec -Name 'worktree' -Description 'Compact worktree listing'
        New-RtkCommandSpec -Name 'help' -Description 'Print this message or the help of the given subcommand(s)'
    )
}

function Get-RtkStaticNode {
    param([string[]]$Path)

    $common = @(Get-RtkCommonOptionTable)
    $key = ($Path -join ' ')

    switch ($key) {
        '' {
            return ConvertTo-RtkNode -Commands (Get-RtkRootCommands) -Options (Get-RtkGlobalOptions)
        }
        'read' {
            $options = @(
                New-RtkOptionSpec -Names @('-l', '--level') -Description 'Filter: none (default, full content), minimal, aggressive' -ValueKind 'Enum' -Metavar 'LEVEL' -Values @('none', 'minimal', 'aggressive')
                New-RtkOptionSpec -Names @('-m', '--max-lines') -Description 'Max lines' -ValueKind 'Integer' -Metavar 'MAX_LINES'
                New-RtkOptionSpec -Names @('--tail-lines') -Description 'Keep only last N lines' -ValueKind 'Integer' -Metavar 'TAIL_LINES'
                New-RtkOptionSpec -Names @('-n', '--line-numbers') -Description 'Show line numbers'
            ) + $common
            return ConvertTo-RtkNode -Options $options -Arguments @(ConvertTo-RtkArgumentSpec -Metavar 'FILES' -Description 'Files to read' -ValueKind 'Path' -Variadic $true)
        }
        'git' {
            $options = @(
                New-RtkOptionSpec -Names @('-C') -Description 'Change to directory before executing' -ValueKind 'Directory' -Metavar 'DIRECTORY'
                New-RtkOptionSpec -Names @('-c') -Description 'Git configuration override (key=value)' -ValueKind 'KeyValue' -Metavar 'CONFIG_OVERRIDE'
                New-RtkOptionSpec -Names @('--git-dir') -Description 'Set the path to the .git directory' -ValueKind 'Path' -Metavar 'GIT_DIR'
                New-RtkOptionSpec -Names @('--work-tree') -Description 'Set the path to the working tree' -ValueKind 'Path' -Metavar 'WORK_TREE'
                New-RtkOptionSpec -Names @('--no-pager') -Description 'Disable pager'
                New-RtkOptionSpec -Names @('--no-optional-locks') -Description 'Skip optional locks'
                New-RtkOptionSpec -Names @('--bare') -Description 'Treat repository as bare'
                New-RtkOptionSpec -Names @('--literal-pathspecs') -Description 'Treat pathspecs literally'
            ) + $common
            return ConvertTo-RtkNode -Commands (Get-RtkGitCommands) -Options $options
        }
        'telemetry' {
            $commands = @(
                New-RtkCommandSpec -Name 'status' -Description 'Show telemetry status'
                New-RtkCommandSpec -Name 'enable' -Description 'Enable telemetry'
                New-RtkCommandSpec -Name 'disable' -Description 'Disable telemetry'
                New-RtkCommandSpec -Name 'forget' -Description 'Forget telemetry data'
                New-RtkCommandSpec -Name 'help' -Description 'Print this message or the help of the given subcommand(s)'
            )
            return ConvertTo-RtkNode -Commands $commands -Options $common
        }
        'hook' {
            $commands = @(
                New-RtkCommandSpec -Name 'claude' -Description 'Process Claude Code PreToolUse hook'
                New-RtkCommandSpec -Name 'cursor' -Description 'Process Cursor Agent hook'
                New-RtkCommandSpec -Name 'gemini' -Description 'Process Gemini CLI BeforeTool hook'
                New-RtkCommandSpec -Name 'copilot' -Description 'Process Copilot preToolUse hook'
                New-RtkCommandSpec -Name 'droid' -Description 'Process Factory Droid PreToolUse hook'
                New-RtkCommandSpec -Name 'vibe' -Description 'Process Mistral Vibe CLI pre_tool hook'
                New-RtkCommandSpec -Name 'check' -Description 'Check how a command would be rewritten by the hook engine'
                New-RtkCommandSpec -Name 'help' -Description 'Print this message or the help of the given subcommand(s)'
            )
            return ConvertTo-RtkNode -Commands $commands -Options $common
        }
        'config' {
            return ConvertTo-RtkNode -Options (@(New-RtkOptionSpec -Names @('--create') -Description 'Create default config file') + $common)
        }
        'json' {
            $options = @(
                New-RtkOptionSpec -Names @('-d', '--depth') -Description 'Max depth' -ValueKind 'Integer' -Metavar 'DEPTH'
                New-RtkOptionSpec -Names @('--keys-only') -Description 'Show keys only'
            ) + $common
            return ConvertTo-RtkNode -Options $options -Arguments @(ConvertTo-RtkArgumentSpec -Metavar 'FILE' -Description 'JSON file' -ValueKind 'Path')
        }
        default {
            return ConvertTo-RtkNode -Options $common -Arguments @(ConvertTo-RtkArgumentSpec -Metavar 'ARGS' -Description 'Arguments passed through' -ValueKind 'Path' -Variadic $true)
        }
    }
}

function Get-RtkExecutable {
    $cache = $script:RtkCompletionCache
    if (-not $cache.ExecutableResolved) {
        $cache.ExecutableResolved = $true
        $command = Get-Command -Name 'rtk' -CommandType Application -ErrorAction Ignore | Select-Object -First 1
        if ($command) {
            $cache.Executable = $command.Source
        }
    }

    $cache.Executable
}

function Invoke-RtkHelp {
    param([string[]]$Path)

    $executable = Get-RtkExecutable
    if (-not $executable) {
        return $null
    }

    $arguments = if ($Path.Count -eq 0) { @('--help') } else { @('help') + $Path }

    try {
        $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
        $startInfo.FileName = $executable
        foreach ($argument in $arguments) {
            $startInfo.ArgumentList.Add($argument)
        }
        $startInfo.UseShellExecute = $false
        $startInfo.CreateNoWindow = $true
        $startInfo.RedirectStandardInput = $true
        $startInfo.RedirectStandardOutput = $true
        $startInfo.RedirectStandardError = $true
        $startInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8
        $startInfo.StandardErrorEncoding = [System.Text.Encoding]::UTF8

        $process = [System.Diagnostics.Process]::Start($startInfo)
        try {
            $process.StandardInput.Close()
            $outputTask = $process.StandardOutput.ReadToEndAsync()
            $errorTask = $process.StandardError.ReadToEndAsync()
            if (-not $process.WaitForExit(5000)) {
                $process.Kill()
                return $null
            }

            $text = ($outputTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '') + ($errorTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '')
            if ([string]::IsNullOrWhiteSpace($text)) {
                return $null
            }

            return $text
        } finally {
            $process.Dispose()
        }
    } catch {
        return $null
    }
}

function ConvertFrom-RtkEnumeratedDescription {
    param([string]$Description)

    if ([string]::IsNullOrWhiteSpace($Description)) {
        return @()
    }

    $text = $Description -replace '\s*\[default:[^\]]*\]', ''
    $text = $text -replace '\([^)]*\)', ''
    if ($text -notmatch ':\s*([^:]+)$') {
        return @()
    }

    $values = @()
    foreach ($part in ($Matches[1] -split ',')) {
        $candidate = $part.Trim().TrimEnd('.')
        if ([string]::IsNullOrEmpty($candidate)) {
            continue
        }

        if ($candidate -notmatch '^[A-Za-z0-9][A-Za-z0-9_.+-]*$') {
            return @()
        }

        $values += $candidate
    }

    if ($values.Count -lt 2 -or $values.Count -gt 16) {
        return @()
    }

    $values
}

function Get-RtkValueKind {
    param(
        [string]$Metavar,
        [string[]]$Values
    )

    if (@($Values).Count -gt 0) {
        return 'Enum'
    }

    if ([string]::IsNullOrEmpty($Metavar)) {
        return 'None'
    }

    if ($Metavar -match '^(DIRECTORY|DIR|CWD|FOLDER)$') {
        return 'Directory'
    }

    if ($Metavar -match '(^|_)(FILE|FILES|PATH|PATHS|OUTPUT|DIR|DIRECTORY)$|^ARGS$') {
        return 'Path'
    }

    if ($Metavar -match '(^|_)(N|NUM|NUMBER|COUNT|LINES|LEN|LENGTH|DEPTH|LIMIT|MAX|MIN|SIZE|WIDTH)$|^MAX') {
        return 'Integer'
    }

    if ($Metavar -match 'OVERRIDE|KEY_VALUE|KV$') {
        return 'KeyValue'
    }

    'Text'
}

function ConvertFrom-RtkHelp {
    param([string]$Text)

    $commands = @()
    $options = @()
    $arguments = @()

    $section = ''
    $entries = @()
    $current = $null

    foreach ($rawLine in ($Text -split "`r?`n")) {
        $line = $rawLine.TrimEnd()

        if ($line -match '^(Commands|Options|Arguments|Usage):\s*(.*)$') {
            if ($current) { $entries += $current; $current = $null }
            $section = $Matches[1]
            continue
        }

        if ($section -eq '' -or $section -eq 'Usage') {
            continue
        }

        if ($line -match '^\S') {
            # A new unindented block ends the current section.
            if ($current) { $entries += $current; $current = $null }
            $section = ''
            continue
        }

        $startsEntry = switch ($section) {
            'Options' { $line -match '^\s{2,7}-\S' }
            'Arguments' { $line -match '^\s{2}[<\[]' }
            default { $line -match '^\s{2}\S' -and $line -notmatch '^\s{3}' }
        }

        if ($startsEntry) {
            if ($current) { $entries += $current }
            $current = [pscustomobject]@{
                Section = $section
                Head    = $line
                Body    = @()
            }
            continue
        }

        if ($current) {
            $current.Body += $line
        }
    }

    if ($current) { $entries += $current }

    foreach ($entry in $entries) {
        $head = $entry.Head
        $bodyText = ''
        $bulletValues = @()
        $inlineValues = @()
        $inPossibleValues = $false

        foreach ($bodyLine in $entry.Body) {
            $trimmed = $bodyLine.Trim()
            if ($trimmed -match '^Possible values:') {
                $inPossibleValues = $true
                continue
            }

            if ($inPossibleValues) {
                if ($trimmed -match '^-\s+([^:\s]+)') {
                    $bulletValues += $Matches[1]
                    continue
                }

                if ($trimmed -eq '') {
                    continue
                }

                if ($trimmed -notmatch '^\[') {
                    $inPossibleValues = $false
                }
            }

            if ($trimmed -ne '') {
                $bodyText = if ($bodyText) { "$bodyText $trimmed" } else { $trimmed }
            }
        }

        if ($bodyText -match '\[possible values:\s*([^\]]+)\]') {
            $inlineValues = @($Matches[1] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        }
        if ($head -match '\[possible values:\s*([^\]]+)\]') {
            $inlineValues = @($Matches[1] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        }

        switch ($entry.Section) {
            'Commands' {
                if ($head -match '^\s{2}(\S+)\s*(.*)$') {
                    $commands += New-RtkCommandSpec $Matches[1] $Matches[2].Trim()
                }
            }
            'Options' {
                if ($head -match '^\s{2,7}(-[^\s<,]+(?:,\s+-[^\s<,]+)*)(?:\s+(?:<([^>]+)>|\[=<([^>]+)>\]))?(?:\s{2,}(\S.*))?$') {
                    $names = @($Matches[1] -split ',\s*' | ForEach-Object { $_.Trim().TrimEnd('.') } | Where-Object { $_ })
                    $metavar = if ($Matches[2]) { $Matches[2] } elseif ($Matches[3]) { $Matches[3] } else { '' }
                    $description = if ($Matches[4]) { $Matches[4].Trim() } else { '' }
                    if ($bodyText) {
                        $description = if ($description) { "$description $bodyText" } else { $bodyText }
                    }

                    $values = @()
                    if ($bulletValues.Count -gt 0) {
                        $values = $bulletValues
                    } elseif ($inlineValues.Count -gt 0) {
                        $values = $inlineValues
                    } elseif ($metavar) {
                        $values = @(ConvertFrom-RtkEnumeratedDescription $description)
                    }

                    $kind = Get-RtkValueKind -Metavar $metavar -Values $values
                    $options += New-RtkOptionSpec -Names $names -Description ($description -replace '\s*\[possible values:[^\]]*\]', '') -ValueKind $kind -Metavar $metavar -Values $values
                }
            }
            'Arguments' {
                if ($head -match '^\s{2}(?:<([^>]+)>|\[([^\]]+)\])(\.\.\.)?(?:\s{2,}(\S.*))?$') {
                    $metavar = if ($Matches[1]) { $Matches[1] } else { $Matches[2] }
                    $variadic = [bool]$Matches[3]
                    $description = if ($Matches[4]) { $Matches[4].Trim() } else { '' }
                    if ($bodyText) {
                        $description = if ($description) { "$description $bodyText" } else { $bodyText }
                    }

                    $values = @()
                    if ($bulletValues.Count -gt 0) {
                        $values = $bulletValues
                    } elseif ($inlineValues.Count -gt 0) {
                        $values = $inlineValues
                    } else {
                        $values = @(ConvertFrom-RtkEnumeratedDescription $description)
                    }

                    $kind = Get-RtkValueKind -Metavar $metavar -Values $values
                    if ($kind -eq 'None') { $kind = 'Text' }
                    $arguments += ConvertTo-RtkArgumentSpec -Metavar $metavar -Description $description -ValueKind $kind -Values $values -Variadic $variadic
                }
            }
        }
    }

    if ($commands.Count -eq 0 -and $options.Count -eq 0 -and $arguments.Count -eq 0) {
        return $null
    }

    ConvertTo-RtkNode -Commands $commands -Options $options -Arguments $arguments -Source 'help'
}

function Get-RtkNode {
    param([string[]]$Path = @())

    $cache = $script:RtkCompletionCache
    $key = ($Path -join ' ')
    if ($cache.Nodes.ContainsKey($key)) {
        return $cache.Nodes[$key]
    }

    $node = $null
    $helpText = Invoke-RtkHelp -Path $Path
    if ($helpText) {
        $node = ConvertFrom-RtkHelp -Text $helpText
    }

    if (-not $node) {
        $node = Get-RtkStaticNode -Path $Path
    }

    $cache.Nodes[$key] = $node
    $node
}

function Get-RtkArgumentTokens {
    param(
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    $tokens = @()
    $first = $true
    foreach ($element in $CommandAst.CommandElements) {
        if ($first) {
            $first = $false
            continue
        }

        if ($element.Extent.EndOffset -ge $CursorPosition) {
            continue
        }

        $text = $element.Extent.Text
        if ([string]::IsNullOrWhiteSpace($text)) {
            continue
        }

        $tokens += $text.Trim([char[]]@([char]34, [char]39))
    }

    $tokens
}

function Get-RtkCurrentWord {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    # The raw text typed so far, opening quote included; the parser keeps an unterminated
    # quoted word as one element running to the cursor.
    foreach ($element in $CommandAst.CommandElements) {
        $extent = $element.Extent
        if ($extent.StartOffset -lt $CursorPosition -and $extent.EndOffset -ge $CursorPosition) {
            return $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset)
        }
    }

    $WordToComplete
}

function Get-RtkQuoteChar {
    param([string]$Value)

    if ($Value -match '^[''"\u2018-\u201E]') { $Value.Substring(0, 1) } else { '' }
}

function ConvertFrom-RtkTypedWord {
    # The value of a typed word. A word opened with a quote (ASCII or typographic) is read by
    # the PowerShell tokenizer, which drops the quotes and undoes that quote style's escapes.
    param([string]$Value)

    if (-not (Get-RtkQuoteChar $Value)) {
        return $Value
    }

    $tokens = $null
    $parseErrors = $null
    [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors)
    $tokens[0].Value
}

function ConvertTo-RtkQuotedValue {
    # Renders a value as one PowerShell argument: bare when safe and no quote was typed,
    # otherwise in the quote the user typed (single by default). PowerShell reads ' and
    # U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes.
    param(
        [string]$Value,
        [string]$QuoteChar = ''
    )

    if (-not $QuoteChar) {
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') {
            return $Value
        }

        $QuoteChar = "'"
    }

    if ($QuoteChar -match '^[''\u2018-\u201B]$') {
        return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar
    }

    $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar
}

function Get-RtkPathCompletions {
    # $WordToComplete is the unquoted path typed so far. The candidate is quoted in $QuoteChar;
    # an attached '--opt=' $Prefix goes inside the quotes when the quote opened the whole word.
    param(
        [string]$WordToComplete,
        [string]$QuoteChar = '',
        [string]$Prefix = '',
        [switch]$PrefixInQuotes,
        [switch]$DirectoryOnly
    )

    $paths = @()
    if ([string]::IsNullOrEmpty($WordToComplete)) {
        $items = @(Get-ChildItem -LiteralPath '.' -Force -ErrorAction Ignore |
            Sort-Object -Property @{ Expression = { -not $_.PSIsContainer } }, Name |
            Select-Object -First 200)

        foreach ($item in $items) {
            $type = if ($item.PSIsContainer) { 'ProviderContainer' } else { 'ProviderItem' }
            $paths += [pscustomobject]@{ Path = '.\' + $item.Name; ListItemText = $item.Name; ToolTip = $item.FullName; ResultType = $type }
        }
    } else {
        # CompleteFilename quotes for PowerShell and wildcard-escapes for -Path parameters
        # (tick``x.txt), so each result is unwrapped by the parser and unescaped before it
        # is quoted once in the style the user typed.
        foreach ($match in [System.Management.Automation.CompletionCompleters]::CompleteFilename($WordToComplete)) {
            $path = $match.CompletionText
            if (Get-RtkQuoteChar $path) {
                $ast = [System.Management.Automation.Language.Parser]::ParseInput($path, [ref]$null, [ref]$null)
                $constant = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)
                if ($constant) {
                    $path = $constant.Value
                }
            }

            $paths += [pscustomobject]@{ Path = [System.Management.Automation.WildcardPattern]::Unescape($path); ListItemText = $match.ListItemText; ToolTip = $match.ToolTip; ResultType = [string]$match.ResultType }
        }
    }

    foreach ($entry in $paths) {
        if ($DirectoryOnly -and $entry.ResultType -ne 'ProviderContainer') {
            continue
        }

        $text = if ($PrefixInQuotes) {
            ConvertTo-RtkQuotedValue -Value ($Prefix + $entry.Path) -QuoteChar $QuoteChar
        } else {
            $Prefix + (ConvertTo-RtkQuotedValue -Value $entry.Path -QuoteChar $QuoteChar)
        }

        New-RtkCompletionResult -CompletionText $text -ToolTip $entry.ToolTip -ResultType $entry.ResultType -ListItemText $entry.ListItemText
    }
}

function Get-RtkOptionMap {
    param([object[]]$Options)

    $map = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal)
    foreach ($option in $Options) {
        foreach ($name in $option.Names) {
            $map[$name] = $option
        }
    }

    $map
}

function Get-RtkValueCompletions {
    param(
        [string]$ValueKind,
        [string[]]$Values,
        [string]$Metavar,
        [string]$WordToComplete,
        [string]$Prefix = '',
        [string]$ToolTipPrefix = '',
        [string]$QuoteChar = '',
        [switch]$PrefixInQuotes
    )

    $candidates = @()
    switch ($ValueKind) {
        'Path' {
            return Get-RtkPathCompletions -WordToComplete $WordToComplete -QuoteChar $QuoteChar -Prefix $Prefix -PrefixInQuotes:$PrefixInQuotes
        }
        'Directory' {
            return Get-RtkPathCompletions -WordToComplete $WordToComplete -QuoteChar $QuoteChar -Prefix $Prefix -PrefixInQuotes:$PrefixInQuotes -DirectoryOnly
        }
        'Enum' {
            $candidates = @($Values)
        }
        'Integer' {
            $candidates = @('<number>', '10', '50', '100', '200')
        }
        'KeyValue' {
            $candidates = @('<name>=<value>', 'core.pager=', 'user.name=', 'user.email=')
        }
        default {
            $placeholder = if ($Metavar) { '<' + $Metavar.ToLowerInvariant() + '>' } else { '<value>' }
            $candidates = @($placeholder)
        }
    }

    foreach ($candidate in $candidates) {
        if (Test-RtkStartsWith $candidate $WordToComplete) {
            $toolTip = if ($ToolTipPrefix) { "$ToolTipPrefix $candidate" } else { $candidate }
            New-RtkCompletionResult -CompletionText ($Prefix + $candidate) -ToolTip $toolTip -ResultType 'ParameterValue' -ListItemText $candidate
        }
    }
}

function Complete-RtkOptionValue {
    param(
        [object]$Option,
        [string]$WordToComplete,
        [string]$Prefix = '',
        [string]$QuoteChar = '',
        [switch]$PrefixInQuotes
    )

    Get-RtkValueCompletions -ValueKind $Option.ValueKind -Values $Option.Values -Metavar $Option.Metavar -WordToComplete $WordToComplete -Prefix $Prefix -ToolTipPrefix "rtk $($Option.Names[-1])" -QuoteChar $QuoteChar -PrefixInQuotes:$PrefixInQuotes
}

function Get-RtkOptionCompletions {
    param(
        [object[]]$Options,
        [string]$WordToComplete
    )

    foreach ($option in $Options) {
        foreach ($name in $option.Names) {
            if (Test-RtkStartsWith $name $WordToComplete) {
                $listText = if ($option.Metavar) { "$name <$($option.Metavar)>" } else { $name }
                New-RtkCompletionResult -CompletionText $name -ToolTip $option.Description -ResultType 'ParameterName' -ListItemText $listText
            }
        }
    }
}

function Complete-RtkCommandName {
    param(
        [object[]]$Commands,
        [string]$WordToComplete
    )

    foreach ($command in $Commands) {
        if (Test-RtkStartsWith $command.Name $WordToComplete) {
            New-RtkCompletionResult -CompletionText $command.Name -ToolTip $command.Description
        }
    }
}

function Get-RtkCommandContext {
    param([string[]]$Tokens)

    $path = @()
    $node = Get-RtkNode -Path @()
    $positionals = @()
    $pendingOption = $null
    $descending = $true
    $optionsEnded = $false

    foreach ($token in $Tokens) {
        if ($null -ne $pendingOption) {
            $pendingOption = $null
            continue
        }

        if (-not $optionsEnded -and $token -eq '--') {
            $optionsEnded = $true
            $descending = $false
            continue
        }

        if (-not $optionsEnded -and $token.StartsWith('-') -and $token.Length -gt 1) {
            $name = $token
            $attached = $false
            $separator = $token.IndexOf('=')
            if ($separator -gt 0) {
                $name = $token.Substring(0, $separator)
                $attached = $true
            }

            $map = Get-RtkOptionMap $node.Options
            if (-not $attached -and $map.ContainsKey($name) -and $map[$name].ValueKind -ne 'None') {
                $pendingOption = $map[$name]
            }

            continue
        }

        if ($descending -and @($node.Commands | Where-Object { $_.Name -eq $token }).Count -gt 0) {
            $path += $token
            $node = Get-RtkNode -Path $path
            continue
        }

        $descending = $false
        $positionals += $token
    }

    [pscustomobject]@{
        Path          = $path
        Node          = $node
        Positionals   = $positionals
        PendingOption = $pendingOption
        Descending    = $descending
        OptionsEnded  = $optionsEnded
    }
}

function Get-RtkArgumentSpecForIndex {
    param(
        [object]$Node,
        [int]$Index
    )

    $arguments = @($Node.Arguments)
    if ($arguments.Count -eq 0) {
        return $null
    }

    if ($Index -lt $arguments.Count) {
        return $arguments[$Index]
    }

    $last = $arguments[$arguments.Count - 1]
    if ($last.Variadic) {
        return $last
    }

    $null
}

function Complete-Rtk {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    $tokens = @(Get-RtkArgumentTokens -CommandAst $CommandAst -CursorPosition $CursorPosition)
    $typedWord = Get-RtkCurrentWord -WordToComplete $WordToComplete -CommandAst $CommandAst -CursorPosition $CursorPosition
    $quoteChar = Get-RtkQuoteChar $typedWord
    $word = ConvertFrom-RtkTypedWord -Value $typedWord
    $context = Get-RtkCommandContext -Tokens $tokens
    $node = $context.Node

    if ($null -ne $context.PendingOption) {
        return Complete-RtkOptionValue -Option $context.PendingOption -WordToComplete $word -QuoteChar $quoteChar
    }

    if (-not $context.OptionsEnded -and $word -match '^(--?[^=\s]+)=(.*)$') {
        $name = $Matches[1]
        $valueWord = $Matches[2]
        $map = Get-RtkOptionMap $node.Options
        if ($map.ContainsKey($name) -and $map[$name].ValueKind -ne 'None') {
            # '--opt=a b' quotes the whole word; --opt='a b' quotes only the value.
            if ($quoteChar) {
                return Complete-RtkOptionValue -Option $map[$name] -WordToComplete $valueWord -Prefix ($name + '=') -QuoteChar $quoteChar -PrefixInQuotes
            }

            return Complete-RtkOptionValue -Option $map[$name] -WordToComplete (ConvertFrom-RtkTypedWord -Value $valueWord) -Prefix ($name + '=') -QuoteChar (Get-RtkQuoteChar $valueWord)
        }

        return @()
    }

    if (-not $context.OptionsEnded -and $word.StartsWith('-')) {
        return Get-RtkOptionCompletions -Options $node.Options -WordToComplete $word
    }

    if ($context.Path.Count -gt 0 -and $context.Path[0] -eq 'help') {
        $helpNode = Get-RtkNode -Path @()
        $helpPath = @()
        foreach ($positional in $context.Positionals) {
            if (@($helpNode.Commands | Where-Object { $_.Name -eq $positional }).Count -eq 0) {
                return @()
            }

            $helpPath += $positional
            $helpNode = Get-RtkNode -Path $helpPath
        }

        return Complete-RtkCommandName -Commands $helpNode.Commands -WordToComplete $word
    }

    if ($context.Descending -and @($node.Commands).Count -gt 0) {
        return Complete-RtkCommandName -Commands $node.Commands -WordToComplete $word
    }

    $argument = Get-RtkArgumentSpecForIndex -Node $node -Index $context.Positionals.Count
    if ($null -ne $argument) {
        $kind = $argument.ValueKind
        if ($kind -eq 'Text' -and $context.Positionals.Count -gt 0) {
            $kind = 'Path'
        }

        return Get-RtkValueCompletions -ValueKind $kind -Values $argument.Values -Metavar $argument.Metavar -WordToComplete $word -ToolTipPrefix $argument.Description -QuoteChar $quoteChar
    }

    if ([string]::IsNullOrEmpty($word) -and -not $context.OptionsEnded) {
        return Get-RtkOptionCompletions -Options $node.Options -WordToComplete '--'
    }

    @()
}

Register-ArgumentCompleter -Native -CommandName @('rtk', 'rtk.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-Rtk -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}