completers/bun_completer/bun_completer.ps1

Set-StrictMode -Version Latest

if (-not (Get-Variable -Name BunCompletionCache -Scope Script -ErrorAction Ignore)) {
    $script:BunCompletionCache = @{
        ExecutablePath       = $null
        ExecutablePathProbed = $false
        HelpDataByPath       = @{}
        PackageJsonByPath    = @{}
        WorkspaceNamesByRoot = @{}
        NodeModulesByRoot    = @{}
        TemplateNamesByRoot  = @{}
        StaticTree           = @{
            ''         = @(
                'run', 'test', 'x', 'repl', 'exec', 'install', 'add', 'remove', 'update', 'audit', 'outdated',
                'link', 'unlink', 'publish', 'patch', 'pm', 'info', 'why', 'list', 'build', 'init', 'create',
                'upgrade', 'dedupe', 'prune'
            )
            'audit'    = @('fix')
            'pm'       = @(
                'scan', 'pack', 'bin', 'ls', 'why', 'whoami', 'view', 'version', 'pkg', 'hash',
                'hash-string', 'hash-print', 'cache', 'migrate', 'untrusted', 'trust', 'default-trusted'
            )
            'pm cache' = @('rm')
            'pm pkg'   = @('get', 'set', 'delete', 'fix')
        }
        StaticDescriptions   = @{
            ''         = @{
                'run'      = 'Execute a file, package.json script, package binary, or system command'
                'test'     = 'Run tests with Bun''s built-in test runner'
                'x'        = 'Execute a package binary and install it if needed (bunx)'
                'repl'     = 'Start an interactive Bun REPL'
                'exec'     = 'Run a shell script directly with Bun'
                'install'  = 'Install dependencies from package.json'
                'add'      = 'Add a dependency to package.json'
                'remove'   = 'Remove a dependency from package.json'
                'update'   = 'Update dependencies'
                'audit'    = 'Check installed packages for vulnerabilities'
                'outdated' = 'Display outdated dependencies'
                'link'     = 'Register or link a local package'
                'unlink'   = 'Unregister a previously linked package'
                'publish'  = 'Publish a package to the npm registry'
                'patch'    = 'Prepare a package for patching or save a patch'
                'pm'       = 'Package manager utilities'
                'info'     = 'View package metadata from the registry'
                'why'      = 'Explain why a package is installed'
                'list'     = 'List installed dependencies'
                'build'    = 'Bundle and transpile source files'
                'init'     = 'Initialize a Bun project'
                'create'   = 'Create a project from a template'
                'upgrade'  = 'Upgrade Bun itself'
                'dedupe'   = 'Remove duplicate versions from the lockfile'
                'prune'    = 'Remove packages that are not in the lockfile from node_modules'
            }
            'audit'    = @{
                'fix' = 'Upgrade vulnerable packages to the lowest safe version that satisfies every dependent''s range'
            }
            'pm'       = @{
                'scan'            = 'Scan packages in the lockfile for security vulnerabilities'
                'pack'            = 'Create a tarball of the current workspace'
                'bin'             = 'Print the bin directory path'
                'ls'              = 'List dependencies from the lockfile'
                'why'             = 'Show dependency tree explaining why a package is installed'
                'whoami'          = 'Print the current npm username'
                'view'            = 'View package metadata from the registry'
                'version'         = 'Bump package.json version and create a git tag'
                'pkg'             = 'Manage package.json data'
                'hash'            = 'Generate and print the current lockfile hash'
                'hash-string'     = 'Print the string used to hash the lockfile'
                'hash-print'      = 'Print the hash stored in the lockfile'
                'cache'           = 'Print or clear Bun''s cache'
                'migrate'         = 'Migrate another package manager lockfile'
                'untrusted'       = 'Print current untrusted dependencies with scripts'
                'trust'           = 'Trust untrusted dependencies and run their scripts'
                'default-trusted' = 'Print Bun''s default trusted dependencies'
            }
            'pm cache' = @{
                'rm' = 'Clear Bun''s global package cache'
            }
            'pm pkg'   = @{
                'get'    = 'Read package.json values'
                'set'    = 'Update package.json values'
                'delete' = 'Delete package.json values'
                'fix'    = 'Auto-correct common package.json issues'
            }
        }
        StaticAliases        = @{
            ''   = @{
                'i'  = 'install'
                'a'  = 'add'
                'r'  = 'remove'
                'rm' = 'remove'
                'c'  = 'create'
            }
            'pm' = @{
                'list' = 'ls'
            }
        }
        StaticOptionValues   = @{
            '--install'                = @('auto', 'fallback', 'force')
            '--dns-result-order'       = @('verbatim', 'ipv4first', 'ipv6first')
            '--unhandled-rejections'   = @('strict', 'throw', 'warn', 'none', 'warn-with-error-code')
            '--backend'                = @('clonefile', 'hardlink', 'symlink', 'copyfile')
            '--omit'                   = @('dev', 'optional', 'peer')
            '--linker'                 = @('isolated', 'hoisted')
            '--audit-level'            = @('low', 'moderate', 'high', 'critical')
            'run|--shell'              = @('bun', 'system')
            'build|--target'           = @('browser', 'bun', 'node')
            'build|--sourcemap'        = @('linked', 'inline', 'external', 'none')
            'build|--format'           = @('esm', 'cjs', 'iife')
            'build|--packages'         = @('external', 'bundle')
            'build|--jsx-runtime'      = @('automatic', 'classic')
            'build|--env'              = @('disable')
            'test|--coverage-reporter' = @('text', 'lcov')
            'test|--reporter'          = @('junit', 'dots')
            'publish|--access'         = @('public', 'restricted')
            'pm pack|--gzip-level'     = @('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
            'init|--react'             = @('tailwind', 'shadcn')
        }
        WorkspaceCacheTtlSeconds = 30
        NodeModulesCacheTtlSeconds = 30
        TemplateCacheTtlSeconds = 60
    }
}

function Get-BunTokenText {
    param([System.Management.Automation.Language.Ast]$Element)

    if ($Element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
        return $Element.Value
    }

    if ($Element -is [System.Management.Automation.Language.CommandParameterAst]) {
        return $Element.Extent.Text
    }

    $Element.Extent.Text
}

function Get-BunUniqueStrings {
    param([string[]]$Items)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $results = New-Object System.Collections.Generic.List[string]

    foreach ($item in @($Items)) {
        if ([string]::IsNullOrWhiteSpace($item)) {
            continue
        }

        if ($seen.Add($item)) {
            [void]$results.Add($item)
        }
    }

    @($results.ToArray())
}

function New-BunSuggestionItem {
    param(
        [string]$CompletionText,
        [string]$ToolTip,
        [string]$ResultType = 'ParameterValue'
    )

    if ([string]::IsNullOrWhiteSpace($CompletionText)) {
        return $null
    }

    [pscustomobject]@{
        CompletionText = $CompletionText
        ToolTip        = if ([string]::IsNullOrWhiteSpace($ToolTip)) { $CompletionText } else { $ToolTip }
        ResultType     = $ResultType
    }
}

function New-BunPathData {
    param(
        [string[]]$Commands = @(),
        [hashtable]$CommandDescriptions = @{},
        [string[]]$Options = @(),
        [hashtable]$ValuesByOption = @{},
        [string[]]$OptionsExpectingValue = @()
    )

    # Each list stays wrapped in @(): a bare command result collapses on assignment, so an empty
    # list would read back as $null and a single-element list as a bare string that '+=' then
    # concatenates instead of appending.
    @{
        Commands              = @(Get-BunUniqueStrings -Items $Commands)
        CommandDescriptions   = $CommandDescriptions
        Options               = @(Get-BunUniqueStrings -Items $Options)
        ValuesByOption        = $ValuesByOption
        OptionsExpectingValue = @(Get-BunUniqueStrings -Items $OptionsExpectingValue)
    }
}

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

    $pathItems = @($Path | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    (($pathItems | ForEach-Object { $_.ToLowerInvariant() }) -join ' ')
}

function Get-BunExecutablePath {
    if ($script:BunCompletionCache.ExecutablePathProbed) {
        return $script:BunCompletionCache.ExecutablePath
    }

    $script:BunCompletionCache.ExecutablePathProbed = $true
    $script:BunCompletionCache.ExecutablePath = $null

    foreach ($commandName in @('bun.exe', 'bun')) {
        $command = Get-Command -Name $commandName -ErrorAction Ignore
        if ($command) {
            $script:BunCompletionCache.ExecutablePath = $command.Source
            break
        }
    }

    $script:BunCompletionCache.ExecutablePath
}

function Invoke-BunCapture {
    param([string[]]$Arguments)

    $executablePath = Get-BunExecutablePath
    if ([string]::IsNullOrWhiteSpace($executablePath)) {
        return @()
    }

    try {
        @(& $executablePath @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' })
    } catch {
        @()
    }
}

function Get-BunOptionTokensFromLine {
    param([string]$Line)

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

    # Version-style dots ('--tls-min-v1.0') are part of the token.
    $tokens = foreach ($match in [regex]::Matches($Line, '(?<!\S)(--?[A-Za-z0-9][A-Za-z0-9\-]*(?:\.[0-9]+)*)')) {
        $match.Groups[1].Value
    }

    Get-BunUniqueStrings -Items $tokens
}

function Test-BunOptionLineExpectsValue {
    param(
        [string]$Line,
        [string[]]$OptionTokens
    )

    if ([string]::IsNullOrWhiteSpace($Line) -or @($OptionTokens).Count -eq 0) {
        return $false
    }

    foreach ($token in @($OptionTokens)) {
        $escapedToken = [regex]::Escape($token)
        if ($Line -match ('{0}(?:\s*=\s*<[^>]+>|\s+<[^>]+>)' -f $escapedToken)) {
            return $true
        }
    }

    $false
}

function Add-BunPossibleValues {
    param(
        [hashtable]$ValueMap,
        [string[]]$OptionTokens,
        [string]$RawLine
    )

    if (@($OptionTokens).Count -eq 0 -or [string]::IsNullOrWhiteSpace($RawLine)) {
        return
    }

    $values = New-Object System.Collections.Generic.List[string]

    foreach ($match in [regex]::Matches($RawLine, '["'']([^"'']+)["'']')) {
        $candidate = $match.Groups[1].Value.Trim()
        if (-not [string]::IsNullOrWhiteSpace($candidate)) {
            [void]$values.Add($candidate)
        }
    }

    # Only an explicit enumeration ('One of ...', 'Valid orders: ...') and quoted literals count;
    # a bare parenthetical such as '(default, auto-installs when no node_modules)' is prose.
    $candidateFragments = New-Object System.Collections.Generic.List[string]
    if ($RawLine -match '(?i)(?:one of|possible values?:|supports either|available:|valid [^:]+:)\s*(.+)$') {
        [void]$candidateFragments.Add($matches[1])
    }

    foreach ($fragment in @($candidateFragments.ToArray())) {
        foreach ($part in ($fragment -split ',|\bor\b|\band/or\b')) {
            $candidate = $part.Trim()
            $candidate = $candidate -replace '\s*\(.*$', ''
            $candidate = $candidate -replace '^(?:default:|default is)\s*', ''
            $candidate = $candidate.Trim('''" .')

            if ([string]::IsNullOrWhiteSpace($candidate) -or $candidate -match '^(?i)default\b') {
                continue
            }

            if ($candidate -match '^[A-Za-z0-9*._/@:+-]+$') {
                [void]$values.Add($candidate)
            }
        }
    }

    $uniqueValues = Get-BunUniqueStrings -Items $values.ToArray()
    foreach ($token in @($OptionTokens)) {
        if (-not $ValueMap.ContainsKey($token)) {
            $ValueMap[$token] = @()
        }

        $ValueMap[$token] = @(Get-BunUniqueStrings -Items ($ValueMap[$token] + $uniqueValues))
    }
}

function Split-BunHelpOptionRow {
    param([string]$Line)

    # A flag row is '<flags> <description>': option tokens and value markers live only in the
    # flag column, so prose such as 'Signal whose handlers run when --watch restarts' cannot
    # register --watch as value-taking or attach SIGTERM to it.
    $parts = @($Line.Trim() -split '\s{2,}', 2)
    [pscustomobject]@{
        Flags       = $parts[0]
        Description = if ($parts.Count -gt 1) { $parts[1] } else { '' }
    }
}

function Get-BunHelpTreeCommandPath {
    param([string]$Text)

    $head = @($Text -split '\s{2,}')[0]
    $words = New-Object System.Collections.Generic.List[string]
    foreach ($word in @($head -split '\s+' | Where-Object { $_ })) {
        # '...' marks the word before it as a repeated argument, not a subcommand.
        if ($word -eq '...') {
            if ($words.Count -gt 0) {
                $words.RemoveAt($words.Count - 1)
            }

            break
        }

        if ($word -notmatch '^[a-z][a-z0-9-]*$') {
            break
        }

        [void]$words.Add($word)
    }

    ($words -join ' ')
}

function Get-BunParsedHelpData {
    param(
        [string[]]$HelpLines,
        [string[]]$Path = @()
    )

    $commands = New-Object System.Collections.Generic.List[string]
    $commandDescriptions = @{}
    $options = New-Object System.Collections.Generic.List[string]
    $valuesByOption = @{}
    $optionsExpectingValue = New-Object System.Collections.Generic.List[string]
    $section = ''
    $pathKey = (@($Path) -join ' ').ToLowerInvariant()
    $treeCommandPath = ''
    $commandColumn = $null

    foreach ($line in @($HelpLines)) {
        if ($line -match '^\s*Commands:\s*$') {
            $section = 'commands'
            $commandColumn = $null
            continue
        }

        if ($line -match '^\s*(Flags|Options):\s*$') {
            $section = 'options'
            continue
        }

        if ($line -match '^\s*(Examples|Arguments):\s*$') {
            $section = ''
            continue
        }

        if ($section -eq 'commands') {
            $trimmedLine = $line.Trim()

            # 'bun pm --help' prints a tree: each 'bun pm <sub> <description>' row is followed by
            # that subcommand's own flags as box-drawing rows.
            if ($trimmedLine -match '^bun\s+(.+)$') {
                $treeRow = $matches[1]
                $treeCommandPath = Get-BunHelpTreeCommandPath -Text $treeRow
                if ($pathKey -and $treeCommandPath.StartsWith("$pathKey ", [System.StringComparison]::Ordinal)) {
                    $childName = @($treeCommandPath.Substring($pathKey.Length + 1) -split ' ')[0]
                    if (-not [string]::IsNullOrWhiteSpace($childName)) {
                        [void]$commands.Add($childName)
                        if ($treeRow -match '\s{2,}(.+)$') {
                            $commandDescriptions[$childName] = $matches[1].Trim()
                        }
                    }
                }

                continue
            }

            # U+251C and U+2514 are the box-drawing glyphs bun hangs a subcommand's flags off; they
            # are written as escapes so this file stays ASCII.
            if ($trimmedLine -match '^[\u251C\u2514]\s*(.+)$') {
                if ($treeCommandPath -ne $pathKey) {
                    continue
                }

                $treeOptionRow = Split-BunHelpOptionRow -Line $matches[1]
                $treeOptionTokens = @(Get-BunOptionTokensFromLine -Line $treeOptionRow.Flags)
                if ($treeOptionTokens.Count -eq 0) {
                    continue
                }

                foreach ($token in $treeOptionTokens) {
                    [void]$options.Add($token)
                }

                if (Test-BunOptionLineExpectsValue -Line $treeOptionRow.Flags -OptionTokens $treeOptionTokens) {
                    foreach ($token in $treeOptionTokens) {
                        [void]$optionsExpectingValue.Add($token)
                    }
                }

                Add-BunPossibleValues -ValueMap $valuesByOption -OptionTokens $treeOptionTokens -RawLine $treeOptionRow.Description
                continue
            }

            # Command rows share one column; a deeper-indented row is an example continuation
            # (' lint Run a package.json script' under 'run'), not a command.
            if ($line -match '^(?<indent>\s{2,})([A-Za-z][A-Za-z0-9-]*)\b(?:\s+.+?)?\s{2,}(.+)$') {
                $indent = $matches['indent'].Length
                if ($null -eq $commandColumn) {
                    $commandColumn = $indent
                }

                if ($indent -ne $commandColumn) {
                    continue
                }

                $commandName = $matches[1]
                $description = $matches[2].Trim()
                [void]$commands.Add($commandName)
                $commandDescriptions[$commandName] = $description
            }

            continue
        }

        if ($section -ne 'options') {
            continue
        }

        $trimmedLine = $line.Trim()
        if ([string]::IsNullOrWhiteSpace($trimmedLine) -or -not $trimmedLine.StartsWith('-')) {
            continue
        }

        $optionRow = Split-BunHelpOptionRow -Line $trimmedLine
        $optionTokens = @(Get-BunOptionTokensFromLine -Line $optionRow.Flags)
        if ($optionTokens.Count -eq 0) {
            continue
        }

        foreach ($token in $optionTokens) {
            [void]$options.Add($token)
        }

        if (Test-BunOptionLineExpectsValue -Line $optionRow.Flags -OptionTokens $optionTokens) {
            foreach ($token in $optionTokens) {
                [void]$optionsExpectingValue.Add($token)
            }
        }

        Add-BunPossibleValues -ValueMap $valuesByOption -OptionTokens $optionTokens -RawLine $optionRow.Description
    }

    New-BunPathData `
        -Commands ($commands.ToArray()) `
        -CommandDescriptions $commandDescriptions `
        -Options ($options.ToArray()) `
        -ValuesByOption $valuesByOption `
        -OptionsExpectingValue ($optionsExpectingValue.ToArray())
}

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

    $cacheKey = Get-BunCacheKey -Path $Path
    if ($script:BunCompletionCache.HelpDataByPath.ContainsKey($cacheKey)) {
        return $script:BunCompletionCache.HelpDataByPath[$cacheKey]
    }

    $commands = @()
    $commandDescriptions = @{}
    if ($script:BunCompletionCache.StaticTree.ContainsKey($cacheKey)) {
        $commands = @($script:BunCompletionCache.StaticTree[$cacheKey])
    }

    if ($script:BunCompletionCache.StaticDescriptions.ContainsKey($cacheKey)) {
        foreach ($entry in $script:BunCompletionCache.StaticDescriptions[$cacheKey].GetEnumerator()) {
            $commandDescriptions[$entry.Key] = $entry.Value
        }
    }

    $helpData = New-BunPathData -Commands $commands -CommandDescriptions $commandDescriptions

    $helpLines = if (@($Path).Count -eq 0) {
        Invoke-BunCapture -Arguments @('--help')
    } else {
        Invoke-BunCapture -Arguments (@($Path) + '--help')
    }

    if (@($helpLines).Count -gt 0) {
        $parsedData = Get-BunParsedHelpData -HelpLines $helpLines -Path $Path

        foreach ($command in @($parsedData.Commands)) {
            if (-not [string]::IsNullOrWhiteSpace($command)) {
                $helpData.Commands += $command
            }
        }

        foreach ($entry in $parsedData.CommandDescriptions.GetEnumerator()) {
            $helpData.CommandDescriptions[$entry.Key] = $entry.Value
        }

        $helpData.Options += $parsedData.Options
        $helpData.OptionsExpectingValue += $parsedData.OptionsExpectingValue

        foreach ($entry in $parsedData.ValuesByOption.GetEnumerator()) {
            if (-not $helpData.ValuesByOption.ContainsKey($entry.Key)) {
                $helpData.ValuesByOption[$entry.Key] = @()
            }

            $helpData.ValuesByOption[$entry.Key] = @(Get-BunUniqueStrings -Items ($helpData.ValuesByOption[$entry.Key] + $entry.Value))
        }
    }

    $helpData.Commands = @(Get-BunUniqueStrings -Items $helpData.Commands)
    $helpData.Options = @(Get-BunUniqueStrings -Items $helpData.Options)
    $helpData.OptionsExpectingValue = @(Get-BunUniqueStrings -Items $helpData.OptionsExpectingValue)

    $script:BunCompletionCache.HelpDataByPath[$cacheKey] = $helpData
    $helpData
}

function Find-BunNearestPackageJsonPath {
    $currentPath = (Get-Location).ProviderPath

    while (-not [string]::IsNullOrWhiteSpace($currentPath)) {
        $candidate = Join-Path -Path $currentPath -ChildPath 'package.json'
        if (Test-Path -LiteralPath $candidate -PathType Leaf) {
            return $candidate
        }

        $parent = Split-Path -Path $currentPath -Parent
        if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $currentPath) {
            break
        }

        $currentPath = $parent
    }

    $null
}

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

    if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return $null
    }

    $item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
    if (-not $item) {
        return $null
    }

    $cacheEntry = if ($script:BunCompletionCache.PackageJsonByPath.ContainsKey($Path)) {
        $script:BunCompletionCache.PackageJsonByPath[$Path]
    } else {
        $null
    }

    if ($cacheEntry -and $cacheEntry.LastWriteTimeUtc -eq $item.LastWriteTimeUtc) {
        return $cacheEntry.Data
    }

    $data = $null
    try {
        $data = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
    } catch {
        $data = $null
    }

    $script:BunCompletionCache.PackageJsonByPath[$Path] = @{
        LastWriteTimeUtc = $item.LastWriteTimeUtc
        Data             = $data
    }

    $data
}

function Get-BunScriptNames {
    $packageJsonPath = Find-BunNearestPackageJsonPath
    if ([string]::IsNullOrWhiteSpace($packageJsonPath)) {
        return @()
    }

    $packageData = Get-BunPackageJsonData -Path $packageJsonPath
    if ($null -eq $packageData -or -not $packageData.ContainsKey('scripts')) {
        return @()
    }

    $scripts = $packageData['scripts']
    if ($scripts -isnot [System.Collections.IDictionary]) {
        return @()
    }

    @($scripts.Keys | Sort-Object -Unique)
}

function Get-BunWorkspaceNames {
    $packageJsonPath = Find-BunNearestPackageJsonPath
    if ([string]::IsNullOrWhiteSpace($packageJsonPath)) {
        return @()
    }

    $projectRoot = Split-Path -Path $packageJsonPath -Parent
    $cacheKey = $projectRoot.ToLowerInvariant()

    if ($script:BunCompletionCache.WorkspaceNamesByRoot.ContainsKey($cacheKey)) {
        $cacheEntry = $script:BunCompletionCache.WorkspaceNamesByRoot[$cacheKey]
        $cacheAge = (Get-Date) - $cacheEntry.UpdatedAt
        if ($cacheAge.TotalSeconds -lt $script:BunCompletionCache.WorkspaceCacheTtlSeconds) {
            return $cacheEntry.Values
        }
    }

    $packageData = Get-BunPackageJsonData -Path $packageJsonPath
    if ($null -eq $packageData -or -not $packageData.ContainsKey('workspaces')) {
        $script:BunCompletionCache.WorkspaceNamesByRoot[$cacheKey] = @{
            UpdatedAt = Get-Date
            Values    = @()
        }
        return @()
    }

    $workspacePatterns = @()
    $workspaces = $packageData['workspaces']
    if ($workspaces -is [System.Collections.IDictionary] -and $workspaces.ContainsKey('packages')) {
        $workspacePatterns = @($workspaces['packages'])
    } elseif ($workspaces -is [System.Collections.IEnumerable] -and $workspaces -isnot [string]) {
        $workspacePatterns = @($workspaces)
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($pattern in @($workspacePatterns)) {
        if ([string]::IsNullOrWhiteSpace([string]$pattern)) {
            continue
        }

        $resolvedPaths = @(Resolve-Path -Path (Join-Path -Path $projectRoot -ChildPath ([string]$pattern)) -ErrorAction SilentlyContinue)
        foreach ($resolvedPath in $resolvedPaths) {
            $item = Get-Item -LiteralPath $resolvedPath.ProviderPath -ErrorAction SilentlyContinue
            if (-not $item) {
                continue
            }

            $workspacePackagePath = if ($item.PSIsContainer) {
                Join-Path -Path $item.FullName -ChildPath 'package.json'
            } elseif ($item.Name -eq 'package.json') {
                $item.FullName
            } else {
                $null
            }

            if ([string]::IsNullOrWhiteSpace($workspacePackagePath) -or -not (Test-Path -LiteralPath $workspacePackagePath -PathType Leaf)) {
                continue
            }

            $workspaceData = Get-BunPackageJsonData -Path $workspacePackagePath
            if ($null -eq $workspaceData) {
                continue
            }

            $workspaceName = if ($workspaceData.ContainsKey('name') -and -not [string]::IsNullOrWhiteSpace([string]$workspaceData['name'])) {
                [string]$workspaceData['name']
            } else {
                Split-Path -Path (Split-Path -Path $workspacePackagePath -Parent) -Leaf
            }

            [void]$names.Add($workspaceName)
        }
    }

    $values = @($names | Sort-Object -Unique)
    $script:BunCompletionCache.WorkspaceNamesByRoot[$cacheKey] = @{
        UpdatedAt = Get-Date
        Values    = $values
    }

    $values
}

function Add-BunPropertyPaths {
    param(
        [object]$Value,
        [string]$Prefix,
        [int]$Depth,
        [System.Collections.Generic.List[string]]$Results
    )

    if ($Depth -le 0 -or $null -eq $Value) {
        return
    }

    if ($Value -is [System.Collections.IDictionary]) {
        foreach ($key in @($Value.Keys | Sort-Object)) {
            $keyText = [string]$key
            $path = if ([string]::IsNullOrWhiteSpace($Prefix)) { $keyText } else { '{0}.{1}' -f $Prefix, $keyText }
            [void]$Results.Add($path)
            Add-BunPropertyPaths -Value $Value[$key] -Prefix $path -Depth ($Depth - 1) -Results $Results
        }

        return
    }

    if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
        if (-not [string]::IsNullOrWhiteSpace($Prefix)) {
            [void]$Results.Add('{0}[]' -f $Prefix)
        }

        $index = 0
        foreach ($item in $Value) {
            if ($index -ge 3) {
                break
            }

            if (-not [string]::IsNullOrWhiteSpace($Prefix)) {
                $indexedPrefix = '{0}[{1}]' -f $Prefix, $index
                [void]$Results.Add($indexedPrefix)
                Add-BunPropertyPaths -Value $item -Prefix $indexedPrefix -Depth ($Depth - 1) -Results $Results
            }

            $index++
        }
    }
}

function Get-BunPackagePropertyPaths {
    $packageJsonPath = Find-BunNearestPackageJsonPath
    if ([string]::IsNullOrWhiteSpace($packageJsonPath)) {
        return @()
    }

    $packageData = Get-BunPackageJsonData -Path $packageJsonPath
    if ($null -eq $packageData) {
        return @()
    }

    $results = New-Object System.Collections.Generic.List[string]
    Add-BunPropertyPaths -Value $packageData -Prefix '' -Depth 4 -Results $results
    Get-BunUniqueStrings -Items $results
}

function Get-BunNodeModulesRoot {
    $packageJsonPath = Find-BunNearestPackageJsonPath
    $currentPath = if ([string]::IsNullOrWhiteSpace($packageJsonPath)) {
        (Get-Location).ProviderPath
    } else {
        Split-Path -Path $packageJsonPath -Parent
    }

    while (-not [string]::IsNullOrWhiteSpace($currentPath)) {
        $candidate = Join-Path -Path $currentPath -ChildPath 'node_modules'
        if (Test-Path -LiteralPath $candidate -PathType Container) {
            return $candidate
        }

        $parent = Split-Path -Path $currentPath -Parent
        if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $currentPath) {
            break
        }

        $currentPath = $parent
    }

    $null
}

function Get-BunInstalledPackageNames {
    $nodeModulesPath = Get-BunNodeModulesRoot
    if ([string]::IsNullOrWhiteSpace($nodeModulesPath)) {
        return @()
    }

    $cacheKey = $nodeModulesPath.ToLowerInvariant()
    if ($script:BunCompletionCache.NodeModulesByRoot.ContainsKey($cacheKey)) {
        $cacheEntry = $script:BunCompletionCache.NodeModulesByRoot[$cacheKey]
        $cacheAge = (Get-Date) - $cacheEntry.UpdatedAt
        if ($cacheAge.TotalSeconds -lt $script:BunCompletionCache.NodeModulesCacheTtlSeconds) {
            return $cacheEntry.Values
        }
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($item in @(Get-ChildItem -LiteralPath $nodeModulesPath -Directory -ErrorAction SilentlyContinue)) {
        if ($item.Name -eq '.bin') {
            continue
        }

        if ($item.Name.StartsWith('@', [System.StringComparison]::Ordinal)) {
            foreach ($child in @(Get-ChildItem -LiteralPath $item.FullName -Directory -ErrorAction SilentlyContinue)) {
                [void]$names.Add("$($item.Name)/$($child.Name)")
            }

            continue
        }

        [void]$names.Add($item.Name)
    }

    $values = @($names | Sort-Object -Unique)
    $script:BunCompletionCache.NodeModulesByRoot[$cacheKey] = @{
        UpdatedAt = Get-Date
        Values    = $values
    }

    $values
}

function Get-BunManifestPackageNames {
    $packageJsonPath = Find-BunNearestPackageJsonPath
    if ([string]::IsNullOrWhiteSpace($packageJsonPath)) {
        return @()
    }

    $packageData = Get-BunPackageJsonData -Path $packageJsonPath
    if ($null -eq $packageData) {
        return @()
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($field in @('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies')) {
        if (-not $packageData.ContainsKey($field)) {
            continue
        }

        $value = $packageData[$field]
        if ($value -is [System.Collections.IDictionary]) {
            foreach ($name in @($value.Keys)) {
                [void]$names.Add([string]$name)
            }
        }
    }

    if ($packageData.ContainsKey('trustedDependencies')) {
        foreach ($name in @($packageData['trustedDependencies'])) {
            if (-not [string]::IsNullOrWhiteSpace([string]$name)) {
                [void]$names.Add([string]$name)
            }
        }
    }

    Get-BunUniqueStrings -Items $names
}

function Get-BunKnownPackageNames {
    Get-BunUniqueStrings -Items ((Get-BunManifestPackageNames) + (Get-BunInstalledPackageNames))
}

function Get-BunNodeModulesBinNames {
    $nodeModulesPath = Get-BunNodeModulesRoot
    if ([string]::IsNullOrWhiteSpace($nodeModulesPath)) {
        return @()
    }

    $binPath = Join-Path -Path $nodeModulesPath -ChildPath '.bin'
    if (-not (Test-Path -LiteralPath $binPath -PathType Container)) {
        return @()
    }

    $names = foreach ($item in @(Get-ChildItem -LiteralPath $binPath -File -ErrorAction SilentlyContinue)) {
        if ($item.BaseName.EndsWith('.cmd', [System.StringComparison]::OrdinalIgnoreCase)) {
            [System.IO.Path]::GetFileNameWithoutExtension($item.BaseName)
        } elseif ($item.Extension -in @('.cmd', '.ps1', '.psm1', '.exe')) {
            $item.BaseName
        } else {
            $item.Name
        }
    }

    Get-BunUniqueStrings -Items $names
}

function Get-BunCreateTemplateNames {
    $currentRoot = (Get-Location).ProviderPath
    $cacheKey = $currentRoot.ToLowerInvariant()

    if ($script:BunCompletionCache.TemplateNamesByRoot.ContainsKey($cacheKey)) {
        $cacheEntry = $script:BunCompletionCache.TemplateNamesByRoot[$cacheKey]
        $cacheAge = (Get-Date) - $cacheEntry.UpdatedAt
        if ($cacheAge.TotalSeconds -lt $script:BunCompletionCache.TemplateCacheTtlSeconds) {
            return $cacheEntry.Values
        }
    }

    $names = New-Object System.Collections.Generic.List[string]
    foreach ($basePath in @(
            (Join-Path -Path $HOME -ChildPath '.bun-create'),
            (Join-Path -Path $currentRoot -ChildPath '.bun-create')
        )) {
        if (-not (Test-Path -LiteralPath $basePath -PathType Container)) {
            continue
        }

        foreach ($item in @(Get-ChildItem -LiteralPath $basePath -Directory -ErrorAction SilentlyContinue)) {
            [void]$names.Add($item.Name)
        }
    }

    $values = @($names | Sort-Object -Unique)
    $script:BunCompletionCache.TemplateNamesByRoot[$cacheKey] = @{
        UpdatedAt = Get-Date
        Values    = $values
    }

    $values
}

function Get-BunQuoteCharacter {
    param([string]$InputText)

    if ([string]::IsNullOrEmpty($InputText)) {
        return $null
    }

    if ($InputText.StartsWith('"')) {
        return '"'
    }

    if ($InputText.StartsWith("'")) {
        return "'"
    }

    $null
}

function Remove-BunOuterQuotes {
    param([string]$InputText)

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

    if ($InputText.Length -ge 2) {
        if (($InputText.StartsWith('"') -and $InputText.EndsWith('"')) -or
            ($InputText.StartsWith("'") -and $InputText.EndsWith("'"))) {
            return $InputText.Substring(1, $InputText.Length - 2)
        }
    }

    $InputText.Trim('"', "'")
}

function ConvertTo-BunQuotedValue {
    param(
        [string]$Value,
        [string]$QuoteCharacter
    )

    if ([string]::IsNullOrWhiteSpace($QuoteCharacter)) {
        if ($Value -match '\s') {
            return '"' + ($Value -replace '"', '\"') + '"'
        }

        return $Value
    }

    if ($QuoteCharacter -eq "'") {
        return "'" + ($Value -replace "'", "''") + "'"
    }

    '"' + ($Value -replace '"', '\"') + '"'
}

function Get-BunPathSuggestions {
    param(
        [string]$InputText,
        [switch]$DirectoryOnly,
        [string[]]$PreferredExtensions = @(),
        [string]$AttachedPrefix = ''
    )

    $text = if ($null -eq $InputText) { '' } else { $InputText }
    $quoteCharacter = Get-BunQuoteCharacter -InputText $text
    $trimmedInput = Remove-BunOuterQuotes -InputText $text

    if ([string]::IsNullOrWhiteSpace($trimmedInput)) {
        $parent = '.'
        $leaf = ''
    } elseif (Test-Path -LiteralPath $trimmedInput -PathType Container) {
        $parent = $trimmedInput
        $leaf = ''
    } else {
        $parent = Split-Path -Path $trimmedInput -Parent
        if ([string]::IsNullOrWhiteSpace($parent)) {
            $parent = '.'
        }

        $leaf = Split-Path -Path $trimmedInput -Leaf
    }

    $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" }
    $preferredMap = @{}
    foreach ($extension in @($PreferredExtensions)) {
        if ([string]::IsNullOrWhiteSpace($extension)) {
            continue
        }

        $preferredMap[$extension.ToLowerInvariant()] = $true
    }

    $items = @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction SilentlyContinue)
    if ($DirectoryOnly) {
        $items = @($items | Where-Object { $_.PSIsContainer })
    }

    $sortedItems = @(
        $items | Sort-Object `
            @{ Expression = { -not $_.PSIsContainer } }, `
            @{ Expression = {
                    if ($_.PSIsContainer) {
                        0
                    } elseif ($preferredMap.ContainsKey($_.Extension.ToLowerInvariant())) {
                        0
                    } else {
                        1
                    }
                }
            }, `
            @{ Expression = { $_.Name } }
    )

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($item in $sortedItems) {
        $completionText = if (-not [System.IO.Path]::IsPathRooted($trimmedInput)) {
            if ($parent -eq '.') {
                $item.Name
            } else {
                Join-Path -Path $parent -ChildPath $item.Name
            }
        } else {
            $item.FullName
        }

        if ($item.PSIsContainer -and -not $completionText.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
            $completionText += [System.IO.Path]::DirectorySeparatorChar
        }

        if (-not [string]::IsNullOrWhiteSpace($quoteCharacter) -or ($completionText -match '\s')) {
            $completionText = ConvertTo-BunQuotedValue -Value $completionText -QuoteCharacter $quoteCharacter
        }

        $toolTip = if ($item.PSIsContainer) {
            'Directory: {0}' -f $item.FullName
        } else {
            $item.FullName
        }

        $suggestion = New-BunSuggestionItem -CompletionText ($AttachedPrefix + $completionText) -ToolTip $toolTip
        if ($suggestion) {
            [void]$results.Add($suggestion)
        }
    }

    @($results.ToArray())
}

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

    $pathKey = Get-BunCacheKey -Path $Path
    $rootOptions = @(
        '--preload', '-r', '--require', '--import', '--inspect', '--inspect-wait', '--inspect-brk',
        '--cpu-prof-name', '--cpu-prof-dir', '--heap-prof-name', '--heap-prof-dir', '--install',
        '--eval', '-e', '--print', '-p', '--port', '--conditions', '--fetch-preconnect',
        '--max-http-header-size', '--dns-result-order', '--title', '--unhandled-rejections',
        '--console-depth', '--user-agent', '--cron-title', '--cron-period', '--env-file',
        '--cwd', '--config', '-c', '--filter', '-F'
    )

    switch ($pathKey) {
        '' {
            return $rootOptions
        }
        'run' {
            return Get-BunUniqueStrings -Items ($rootOptions + @(
                    '--shell', '--main-fields', '--extension-order', '--tsconfig-override',
                    '--define', '-d', '--drop', '--loader', '-l', '--jsx-factory',
                    '--jsx-fragment', '--jsx-import-source', '--jsx-runtime'
                ))
        }
        'build' {
            return @(
                '--compile-exec-argv', '--compile-executable-path', '--target', '--outdir', '--outfile',
                '--metafile', '--metafile-md', '--sourcemap', '--banner', '--footer', '--format',
                '--root', '--public-path', '--external', '-e', '--allow-unresolved', '--packages',
                '--entry-naming', '--chunk-naming', '--asset-naming', '--conditions', '--env',
                '--windows-icon', '--windows-title', '--windows-publisher', '--windows-version',
                '--windows-description', '--windows-copyright'
            )
        }
        'test' {
            return @(
                '--timeout', '--rerun-each', '--retry', '--seed', '--coverage-reporter', '--coverage-dir',
                '--bail', '-t', '--test-name-pattern', '--reporter', '--reporter-outfile',
                '--max-concurrency', '--path-ignore-patterns'
            )
        }
        'install' { return @('--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os', '--config', '-c') }
        'add'     { return @('--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os', '--config', '-c') }
        'remove'  { return @('--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os', '--config', '-c') }
        'update'  { return @('--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os', '--config', '-c', '--filter') }
        'link'    { return @('--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os', '--config', '-c') }
        'publish' {
            return @(
                '--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts',
                '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os',
                '--config', '-c', '--access', '--tag', '--otp', '--auth-type', '--gzip-level'
            )
        }
        'patch' {
            return @(
                '--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts',
                '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os',
                '--config', '-c', '--patches-dir'
            )
        }
        'outdated' {
            return @(
                '--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts',
                '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os',
                '--config', '-c', '--filter', '-F'
            )
        }
        'info' {
            return @(
                '--ca', '--cafile', '--cache-dir', '--cwd', '--backend', '--registry', '--concurrent-scripts',
                '--network-concurrency', '--omit', '--linker', '--minimum-release-age', '--cpu', '--os',
                '--config', '-c'
            )
        }
        'audit' {
            return @('--audit-level', '--ignore')
        }
        'why' {
            return @('--depth')
        }
        'init' {
            return @('--react')
        }
        'x' {
            return @('-p', '--package')
        }
        'pm pack' {
            return @('--destination', '--filename', '--gzip-level')
        }
        'pm version' {
            return @('--message', '-m', '--preid')
        }
        default {
            return @()
        }
    }
}

function Get-BunPathOptionKind {
    param([string]$Option)

    if ([string]::IsNullOrWhiteSpace($Option)) {
        return $null
    }

    switch ($Option.ToLowerInvariant()) {
        '--cwd' { 'directory'; break }
        '--cpu-prof-dir' { 'directory'; break }
        '--heap-prof-dir' { 'directory'; break }
        '--outdir' { 'directory'; break }
        '--coverage-dir' { 'directory'; break }
        '--destination' { 'directory'; break }
        '--cache-dir' { 'directory'; break }
        '--patches-dir' { 'directory'; break }
        '--config' { 'file'; break }
        '-c' { 'file'; break }
        '--env-file' { 'file'; break }
        '--preload' { 'file'; break }
        '-r' { 'file'; break }
        '--require' { 'file'; break }
        '--import' { 'file'; break }
        '--tsconfig-override' { 'file'; break }
        '--outfile' { 'file'; break }
        '--metafile' { 'file'; break }
        '--metafile-md' { 'file'; break }
        '--windows-icon' { 'file'; break }
        '--cafile' { 'file'; break }
        '--reporter-outfile' { 'file'; break }
        '--filename' { 'file'; break }
        default { $null }
    }
}

function Get-BunOptionExpectsValue {
    param(
        [string[]]$Path,
        [string]$Option
    )

    if ([string]::IsNullOrWhiteSpace($Option) -or -not $Option.StartsWith('-')) {
        return $false
    }

    # An empty option list collapses to AutomationNull on its way into the cache, so @() around it
    # yields a single $null element; drop that before calling a method on it.
    $staticOptions = Get-BunStaticOptionsExpectingValue -Path $Path
    foreach ($candidate in @($staticOptions | Where-Object { $_ })) {
        if ($candidate.Equals($Option, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }

    $helpData = Get-BunHelpData -Path $Path
    foreach ($candidate in @($helpData.OptionsExpectingValue | Where-Object { $_ })) {
        if ($candidate.Equals($Option, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }

    $false
}

function Find-BunSubcommand {
    param(
        [string[]]$Path,
        [string]$Token
    )

    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $null
    }

    $helpData = Get-BunHelpData -Path $Path
    foreach ($command in @($helpData.Commands)) {
        if ([string]::IsNullOrWhiteSpace($command)) {
            continue
        }

        if ($command.Equals($Token, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $command
        }
    }

    $pathKey = Get-BunCacheKey -Path $Path
    if ($script:BunCompletionCache.StaticAliases.ContainsKey($pathKey)) {
        foreach ($entry in $script:BunCompletionCache.StaticAliases[$pathKey].GetEnumerator()) {
            if ($entry.Key.Equals($Token, [System.StringComparison]::OrdinalIgnoreCase)) {
                return $entry.Value
            }
        }
    }

    $null
}

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

    if ($CommandAst.CommandElements.Count -gt 0) {
        $lastElement = $CommandAst.CommandElements[$CommandAst.CommandElements.Count - 1]
        if ($CursorPosition -gt $lastElement.Extent.EndOffset) {
            return ''
        }
    }

    if (-not [string]::IsNullOrEmpty($WordToComplete)) {
        return $WordToComplete
    }

    if ($CommandAst.CommandElements.Count -eq 0) {
        return $WordToComplete
    }

    $lastElement = $CommandAst.CommandElements[$CommandAst.CommandElements.Count - 1]
    if ($lastElement.Extent.EndOffset -ne $CursorPosition) {
        return $WordToComplete
    }

    Get-BunTokenText -Element $lastElement
}

function Get-BunOptionAssignmentContext {
    param([string]$WordToComplete)

    if ($WordToComplete -match '^(--?[A-Za-z0-9][A-Za-z0-9\-]*)=(.*)$') {
        return @{
            Option      = $matches[1]
            ValuePrefix = $matches[2]
        }
    }

    $null
}

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

    $hasTrailingSpace = $false
    if ($CommandAst.CommandElements.Count -gt 0) {
        $lastElement = $CommandAst.CommandElements[$CommandAst.CommandElements.Count - 1]
        $hasTrailingSpace = $CursorPosition -gt $lastElement.Extent.EndOffset
    }

    $tokens = New-Object System.Collections.Generic.List[string]
    for ($index = 1; $index -lt $CommandAst.CommandElements.Count; $index++) {
        $element = $CommandAst.CommandElements[$index]
        if ($element.Extent.StartOffset -ge $CursorPosition) {
            continue
        }

        $token = Get-BunTokenText -Element $element
        if ([string]::IsNullOrWhiteSpace($token)) {
            continue
        }

        [void]$tokens.Add($token)
    }

    $tokensBeforeCurrent = @()
    $previousToken = $null
    if ($hasTrailingSpace) {
        $tokensBeforeCurrent = $tokens.ToArray()
        if ($tokens.Count -gt 0) {
            $previousToken = $tokens[$tokens.Count - 1]
        }
    } elseif ($tokens.Count -gt 0) {
        if ($tokens.Count -gt 1) {
            $tokensBeforeCurrent = $tokens.GetRange(0, $tokens.Count - 1).ToArray()
            $previousToken = $tokens[$tokens.Count - 2]
        }
    }

    $path = New-Object System.Collections.Generic.List[string]
    $positionals = New-Object System.Collections.Generic.List[string]
    $afterDoubleDash = $false
    $expectingValueFor = $null

    foreach ($token in @($tokensBeforeCurrent)) {
        if ($afterDoubleDash) {
            [void]$positionals.Add($token)
            continue
        }

        if ($null -ne $expectingValueFor) {
            $expectingValueFor = $null
            continue
        }

        if ($token -eq '--') {
            $afterDoubleDash = $true
            continue
        }

        if ($token -match '^(--?[A-Za-z0-9][A-Za-z0-9\-]*)=') {
            continue
        }

        if ($token.StartsWith('-')) {
            if (Get-BunOptionExpectsValue -Path $path.ToArray() -Option $token) {
                $expectingValueFor = $token
            }

            continue
        }

        $nextCommand = if ($positionals.Count -eq 0) {
            Find-BunSubcommand -Path $path.ToArray() -Token $token
        } else {
            $null
        }

        if ($nextCommand) {
            [void]$path.Add($nextCommand)
        } else {
            [void]$positionals.Add($token)
        }
    }

    if (-not $hasTrailingSpace -and $expectingValueFor) {
        $previousToken = $expectingValueFor
    }

    @{
        Path            = [string[]]@($path.ToArray())
        Positionals     = [string[]]@($positionals.ToArray())
        PreviousToken   = $previousToken
        AfterDoubleDash = $afterDoubleDash
        HasTrailingSpace = $hasTrailingSpace
    }
}

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

    $helpData = Get-BunHelpData -Path $Path
    $items = New-Object System.Collections.Generic.List[object]

    foreach ($command in @($helpData.Commands)) {
        if ([string]::IsNullOrWhiteSpace($command)) {
            continue
        }

        $toolTip = if ($helpData.CommandDescriptions.ContainsKey($command)) {
            $helpData.CommandDescriptions[$command]
        } else {
            $command
        }

        $item = New-BunSuggestionItem -CompletionText $command -ToolTip $toolTip
        if ($item) {
            [void]$items.Add($item)
        }
    }

    @($items.ToArray())
}

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

    # Option-name floor from the bun 1.4.2 help, unioned with the parsed help so a broken or
    # missing help capture never leaves a path without options.
    $pathKey = Get-BunCacheKey -Path $Path
    $rootOptions = @(
        '--silent', '--elide-lines', '--filter', '-F', '-b', '--bun', '--no-orphans', '--shell', '--workspaces',
        '--parallel', '--sequential', '--no-exit-on-error', '--watch', '--watch-kill-signal', '--hot',
        '--no-clear-screen', '--smol', '--interactive', '-r', '--preload', '--require', '--import', '--inspect',
        '--inspect-wait', '--inspect-brk', '--cpu-prof', '--cpu-prof-name', '--cpu-prof-dir', '--cpu-prof-md',
        '--cpu-prof-interval', '--heap-prof', '--heap-prof-name', '--heap-prof-dir', '--heap-prof-md',
        '--heap-prof-interval', '--if-present', '--no-install', '--install', '-i', '-e', '--eval', '-p', '--print',
        '--prefer-offline', '--prefer-latest', '--port', '--conditions', '--fetch-preconnect',
        '--experimental-http2-fetch', '--experimental-http3-fetch', '--max-http-header-size',
        '--insecure-http-parser', '--dns-result-order', '--experimental-stream-iter', '--expose-gc',
        '--no-deprecation', '--throw-deprecation', '--no-warnings', '--trace-warnings', '--trace-deprecation',
        '--pending-deprecation', '--redirect-warnings', '--disable-warning', '--title', '--zero-fill-buffers',
        '--use-system-ca', '--use-openssl-ca', '--use-bundled-ca', '--tls-min-v1.0', '--tls-min-v1.1',
        '--tls-min-v1.2', '--tls-min-v1.3', '--tls-max-v1.2', '--tls-max-v1.3', '--redis-preconnect',
        '--sql-preconnect', '--no-addons', '--no-ffi-cc', '--unhandled-rejections', '--console-depth',
        '--user-agent', '--cron-title', '--cron-period', '--main-fields', '--preserve-symlinks',
        '--preserve-symlinks-main', '--extension-order', '--tsconfig-override', '-d', '--define', '--drop',
        '--feature', '-l', '--loader', '--no-macros', '--jsx-factory', '--jsx-fragment', '--jsx-import-source',
        '--jsx-runtime', '--jsx-side-effects', '--ignore-dce-annotations', '--env-file', '--no-env-file', '--cwd',
        '-c', '--config', '-h', '--help'
    )
    $installFamily = @(
        '-c', '--config', '-y', '--yarn', '-p', '--production', '--no-save', '--save', '--ca', '--cafile', '--dry-run',
        '--frozen-lockfile', '-f', '--force', '--cache-dir', '--no-cache', '--silent', '--quiet', '--verbose',
        '--no-progress', '--no-summary', '--no-verify', '--offline', '--prefer-offline', '--ignore-scripts', '--trust',
        '-g', '--global', '--cwd', '--backend', '--registry', '--concurrent-scripts', '--network-concurrency',
        '--save-text-lockfile', '--omit', '--lockfile-only', '--linker', '--minimum-release-age', '--cpu', '--os',
        '-h', '--help'
    )

    switch ($pathKey) {
        '' { return $rootOptions }
        'run' { return $rootOptions }
        'test' {
            return @(
                '--no-orphans', '--timeout', '-u', '--update-snapshots', '--rerun-each', '--retry', '--todo', '--only',
                '--pass-with-no-tests', '--concurrent', '--randomize', '--seed', '--coverage', '--coverage-reporter',
                '--coverage-dir', '--bail', '-t', '--test-name-pattern', '--reporter', '--reporter-outfile', '--dots',
                '--only-failures', '--max-concurrency', '--path-ignore-patterns', '--changed', '--isolate',
                '--no-isolate', '--parallel', '--parallel-delay', '--test-worker', '--shard', '--timings',
                '--update-timings'
            )
        }
        'build' {
            return @(
                '--production', '--compile', '--compile-exec-argv', '--compile-autoload-dotenv',
                '--no-compile-autoload-dotenv', '--compile-autoload-bunfig', '--no-compile-autoload-bunfig',
                '--compile-autoload-tsconfig', '--no-compile-autoload-tsconfig', '--compile-autoload-package-json',
                '--no-compile-autoload-package-json', '--compile-executable-path', '--asset', '--bytecode',
                '--bytecode-depth', '--watch', '--no-clear-screen', '--target', '--outdir', '--outfile', '--metafile',
                '--metafile-md', '--sourcemap', '--banner', '--footer', '--format', '--root', '--splitting',
                '--no-split-require', '--no-module-preload', '--min-chunk-size', '--public-path', '-e', '--external',
                '--allow-unresolved', '--reject-unresolved', '--packages', '--entry-naming', '--chunk-naming',
                '--asset-naming', '--react-fast-refresh', '--react-compiler', '--no-bundle', '--emit-dce-annotations',
                '--minify-whitespace', '--no-deprecated-namespace-object-setters', '--minify', '--minify-syntax',
                '--minify-identifiers', '--keep-names', '--css-chunking', '--conditions', '--app',
                '--server-components', '--env', '--windows-hide-console', '--windows-icon', '--windows-title',
                '--windows-publisher', '--windows-version', '--windows-description', '--windows-copyright'
            )
        }
        'install' { return $installFamily + @('-d', '--dev', '--optional', '--peer', '-E', '--exact', '--filter', '-a', '--analyze', '--only-missing', '--catalog') }
        'add' { return $installFamily + @('-d', '--dev', '--optional', '--peer', '-E', '--exact', '--filter', '-a', '--analyze', '--only-missing', '--catalog') }
        'remove' { return $installFamily + @('--filter') }
        'update' { return $installFamily + @('--prod', '-L', '--latest', '-i', '--interactive', '--filter', '-r', '--recursive', '-d', '--dev', '--no-optional', '-E', '--exact') }
        'outdated' { return $installFamily + @('--filter', '-r', '--recursive') }
        'publish' { return $installFamily + @('--access', '--tag', '--otp', '--auth-type', '--gzip-level', '--tolerate-republish') }
        'link' { return $installFamily }
        'info' { return $installFamily + @('--json') }
        'patch' { return $installFamily + @('--commit', '--patches-dir') }
        'audit' { return @('--json', '--audit-level', '--ignore', '-L', '--latest') }
        'audit fix' { return @('--dry-run', '-L', '--latest') }
        'why' { return @('--top', '--depth') }
        'init' { return @('--help', '-y', '--yes', '-m', '--minimal', '-r', '--react') }
        'pm pack' { return @('--dry-run', '--destination', '--filename', '--ignore-scripts', '--gzip-level', '--quiet') }
        default { return @() }
    }
}

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

    $helpData = Get-BunHelpData -Path $Path
    $items = New-Object System.Collections.Generic.List[object]

    foreach ($option in @(Get-BunUniqueStrings -Items (@($helpData.Options) + @(Get-BunStaticOptionNameList -Path $Path)))) {
        $item = New-BunSuggestionItem -CompletionText $option -ToolTip $option -ResultType 'ParameterName'
        if ($item) {
            [void]$items.Add($item)
        }
    }

    @($items.ToArray())
}

function Get-BunStaticOptionValueSuggestions {
    param(
        [string[]]$Path,
        [string]$Option,
        [string]$AssignmentPrefix = ''
    )

    $items = New-Object System.Collections.Generic.List[object]
    $pathKey = Get-BunCacheKey -Path $Path
    $lookupKeys = @()

    if (-not [string]::IsNullOrWhiteSpace($pathKey)) {
        $lookupKeys += ('{0}|{1}' -f $pathKey, $Option.ToLowerInvariant())
    }

    $lookupKeys += $Option.ToLowerInvariant()

    foreach ($lookupKey in @($lookupKeys)) {
        if (-not $script:BunCompletionCache.StaticOptionValues.ContainsKey($lookupKey)) {
            continue
        }

        foreach ($value in @($script:BunCompletionCache.StaticOptionValues[$lookupKey])) {
            $item = New-BunSuggestionItem -CompletionText ($AssignmentPrefix + $value) -ToolTip ('{0} value' -f $Option)
            if ($item) {
                [void]$items.Add($item)
            }
        }
    }

    @($items.ToArray())
}

function Get-BunHelpOptionValueSuggestions {
    param(
        [string[]]$Path,
        [string]$Option,
        [string]$AssignmentPrefix = ''
    )

    $items = New-Object System.Collections.Generic.List[object]
    foreach ($helpPath in @($Path, @())) {
        $helpData = Get-BunHelpData -Path $helpPath
        if (-not $helpData.ValuesByOption.ContainsKey($Option)) {
            continue
        }

        foreach ($value in @($helpData.ValuesByOption[$Option])) {
            $item = New-BunSuggestionItem -CompletionText ($AssignmentPrefix + $value) -ToolTip ('{0} value' -f $Option)
            if ($item) {
                [void]$items.Add($item)
            }
        }
    }

    @($items.ToArray())
}

function Get-BunOptionValueSuggestions {
    param(
        [string[]]$Path,
        [string[]]$Positionals,
        [string]$Option,
        [string]$WordToComplete,
        [string]$AssignmentPrefix = ''
    )

    $items = New-Object System.Collections.Generic.List[object]
    $normalizedOption = if ([string]::IsNullOrWhiteSpace($Option)) { '' } else { $Option.ToLowerInvariant() }
    $pathKey = Get-BunCacheKey -Path $Path

    foreach ($item in @(Get-BunHelpOptionValueSuggestions -Path $Path -Option $Option -AssignmentPrefix $AssignmentPrefix)) {
        [void]$items.Add($item)
    }

    foreach ($item in @(Get-BunStaticOptionValueSuggestions -Path $Path -Option $Option -AssignmentPrefix $AssignmentPrefix)) {
        [void]$items.Add($item)
    }

    switch ($pathKey) {
        { $_ -in @('', 'run') } {
            # -F/--filter is a root flag ('bun --filter <pattern> <script>') inherited by run.
            if ($normalizedOption -in @('--filter', '-f')) {
                foreach ($workspace in @(Get-BunWorkspaceNames)) {
                    $item = New-BunSuggestionItem -CompletionText ($AssignmentPrefix + $workspace) -ToolTip 'workspace name'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'update' {
            if ($normalizedOption -eq '--filter') {
                foreach ($workspace in @(Get-BunWorkspaceNames)) {
                    $item = New-BunSuggestionItem -CompletionText ($AssignmentPrefix + $workspace) -ToolTip 'workspace filter'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'outdated' {
            if ($normalizedOption -in @('--filter', '-f')) {
                foreach ($workspace in @(Get-BunWorkspaceNames)) {
                    $item = New-BunSuggestionItem -CompletionText ($AssignmentPrefix + $workspace) -ToolTip 'workspace filter'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
    }

    $pathOptionKind = Get-BunPathOptionKind -Option $Option
    if ($pathOptionKind -eq 'directory') {
        foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -DirectoryOnly -AttachedPrefix $AssignmentPrefix)) {
            [void]$items.Add($item)
        }
    } elseif ($pathOptionKind -eq 'file') {
        $preferredExtensions = switch ($normalizedOption) {
            '--config' { @('.toml') }
            '-c' { @('.toml') }
            '--env-file' { @('.env') }
            '--preload' { @('.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx') }
            '-r' { @('.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx') }
            '--require' { @('.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx') }
            '--import' { @('.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx') }
            '--tsconfig-override' { @('.json') }
            '--reporter-outfile' { @('.xml') }
            '--windows-icon' { @('.ico') }
            '--filename' { @('.tgz') }
            default { @() }
        }

        foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $preferredExtensions -AttachedPrefix $AssignmentPrefix)) {
            [void]$items.Add($item)
        }
    }

    @($items.ToArray())
}

function Get-BunPositionalSuggestions {
    param(
        [string[]]$Path,
        [string[]]$Positionals,
        [bool]$AfterDoubleDash,
        [string]$WordToComplete = ''
    )

    if ($AfterDoubleDash) {
        return @()
    }

    $items = New-Object System.Collections.Generic.List[object]
    $pathKey = Get-BunCacheKey -Path $Path
    $positionIndex = @($Positionals).Count
    $sourceExtensions = @('.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx', '.json')
    $tarballExtensions = @('.tgz', '.tar.gz')

    switch ($pathKey) {
        '' {
            foreach ($scriptName in @(Get-BunScriptNames)) {
                $item = New-BunSuggestionItem -CompletionText $scriptName -ToolTip 'package.json script'
                if ($item) {
                    [void]$items.Add($item)
                }
            }

            foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $sourceExtensions)) {
                [void]$items.Add($item)
            }
        }
        'run' {
            if ($positionIndex -eq 0) {
                foreach ($scriptName in @(Get-BunScriptNames)) {
                    $item = New-BunSuggestionItem -CompletionText $scriptName -ToolTip 'package.json script'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }

                foreach ($binName in @(Get-BunNodeModulesBinNames)) {
                    $item = New-BunSuggestionItem -CompletionText $binName -ToolTip 'local package binary'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }

                foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $sourceExtensions)) {
                    [void]$items.Add($item)
                }
            }
        }
        'build' {
            foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $sourceExtensions)) {
                [void]$items.Add($item)
            }
        }
        'test' {
            foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $sourceExtensions)) {
                [void]$items.Add($item)
            }
        }
        'init' {
            if ($positionIndex -eq 0) {
                foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -DirectoryOnly)) {
                    [void]$items.Add($item)
                }
            }
        }
        'create' {
            if ($positionIndex -eq 0) {
                foreach ($templateName in @(Get-BunCreateTemplateNames)) {
                    $item = New-BunSuggestionItem -CompletionText $templateName -ToolTip 'local bun create template'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }

                foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions @('.jsx', '.tsx'))) {
                    [void]$items.Add($item)
                }
            } elseif ($positionIndex -eq 1) {
                foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -DirectoryOnly)) {
                    [void]$items.Add($item)
                }
            }
        }
        'remove' {
            foreach ($packageName in @(Get-BunKnownPackageNames)) {
                $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
        'update' {
            foreach ($packageName in @(Get-BunKnownPackageNames)) {
                $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
        'info' {
            if ($positionIndex -eq 0) {
                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            } elseif ($positionIndex -eq 1) {
                foreach ($propertyPath in @(Get-BunPackagePropertyPaths)) {
                    $item = New-BunSuggestionItem -CompletionText $propertyPath -ToolTip 'package.json property path'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'why' {
            if ($positionIndex -eq 0) {
                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            } elseif ($positionIndex -eq 1) {
                foreach ($propertyPath in @(Get-BunPackagePropertyPaths)) {
                    $item = New-BunSuggestionItem -CompletionText $propertyPath -ToolTip 'package.json property path'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'patch' {
            if ($positionIndex -eq 0) {
                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'outdated' {
            if ($positionIndex -eq 0) {
                foreach ($workspace in @(Get-BunWorkspaceNames)) {
                    $item = New-BunSuggestionItem -CompletionText $workspace -ToolTip 'workspace filter'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }

                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'publish' {
            if ($positionIndex -eq 0) {
                foreach ($item in @(Get-BunPathSuggestions -InputText $WordToComplete -PreferredExtensions $tarballExtensions)) {
                    [void]$items.Add($item)
                }
            }
        }
        'x' {
            if ($positionIndex -eq 0) {
                foreach ($binName in @(Get-BunNodeModulesBinNames)) {
                    $item = New-BunSuggestionItem -CompletionText $binName -ToolTip 'local package binary'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }

                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'pm view' {
            if ($positionIndex -eq 0) {
                foreach ($packageName in @(Get-BunKnownPackageNames)) {
                    $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'pm trust' {
            foreach ($packageName in @(Get-BunKnownPackageNames)) {
                $item = New-BunSuggestionItem -CompletionText $packageName -ToolTip 'known package'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
        'pm version' {
            if ($positionIndex -eq 0) {
                foreach ($value in @('patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease', 'from-git')) {
                    $item = New-BunSuggestionItem -CompletionText $value -ToolTip 'version increment'
                    if ($item) {
                        [void]$items.Add($item)
                    }
                }
            }
        }
        'pm pkg get' {
            foreach ($propertyPath in @(Get-BunPackagePropertyPaths)) {
                $item = New-BunSuggestionItem -CompletionText $propertyPath -ToolTip 'package.json property path'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
        'pm pkg delete' {
            foreach ($propertyPath in @(Get-BunPackagePropertyPaths)) {
                $item = New-BunSuggestionItem -CompletionText $propertyPath -ToolTip 'package.json property path'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
        'pm pkg set' {
            foreach ($propertyPath in @(Get-BunPackagePropertyPaths)) {
                $item = New-BunSuggestionItem -CompletionText ('{0}=' -f $propertyPath) -ToolTip 'package.json property assignment'
                if ($item) {
                    [void]$items.Add($item)
                }
            }
        }
    }

    @($items.ToArray())
}

function ConvertTo-BunCompletionResults {
    param(
        [object[]]$Items,
        [string]$WordToComplete
    )

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($item in @($Items)) {
        if ($null -eq $item -or [string]::IsNullOrWhiteSpace($item.CompletionText)) {
            continue
        }

        if (-not [string]::IsNullOrEmpty($WordToComplete) -and
            $item.CompletionText -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            continue
        }

        if (-not $seen.Add($item.CompletionText)) {
            continue
        }

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

function Complete-Bun {
    param($wordToComplete, $commandAst, $cursorPosition)

    $effectiveWordToComplete = Get-BunEffectiveWordToComplete `
        -WordToComplete $wordToComplete `
        -CommandAst $commandAst `
        -CursorPosition $cursorPosition

    $context = Get-BunCommandContext `
        -WordToComplete $effectiveWordToComplete `
        -CommandAst $commandAst `
        -CursorPosition $cursorPosition

    $path = $context.Path
    $positionals = $context.Positionals
    $previousToken = $context.PreviousToken
    $assignmentContext = Get-BunOptionAssignmentContext -WordToComplete $effectiveWordToComplete

    if ($assignmentContext -and (Get-BunOptionExpectsValue -Path $path -Option $assignmentContext.Option)) {
        $items = Get-BunOptionValueSuggestions `
            -Path $path `
            -Positionals $positionals `
            -Option $assignmentContext.Option `
            -WordToComplete $assignmentContext.ValuePrefix `
            -AssignmentPrefix ('{0}=' -f $assignmentContext.Option)

        ConvertTo-BunCompletionResults -Items $items -WordToComplete $effectiveWordToComplete
        return
    }

    if ($previousToken -and (Get-BunOptionExpectsValue -Path $path -Option $previousToken)) {
        $items = Get-BunOptionValueSuggestions `
            -Path $path `
            -Positionals $positionals `
            -Option $previousToken `
            -WordToComplete $effectiveWordToComplete

        ConvertTo-BunCompletionResults -Items $items -WordToComplete $effectiveWordToComplete
        return
    }

    $items = New-Object System.Collections.Generic.List[object]

    if (-not $context.AfterDoubleDash -and -not $effectiveWordToComplete.StartsWith('-')) {
        if (@($positionals).Count -eq 0) {
            foreach ($item in @(Get-BunCommandSuggestions -Path $path)) {
                [void]$items.Add($item)
            }
        }

        foreach ($item in @(Get-BunPositionalSuggestions -Path $path -Positionals $positionals -AfterDoubleDash $context.AfterDoubleDash -WordToComplete $effectiveWordToComplete)) {
            [void]$items.Add($item)
        }
    }

    if (-not $context.AfterDoubleDash) {
        foreach ($item in @(Get-BunOptionSuggestions -Path $path)) {
            [void]$items.Add($item)
        }
    }

    ConvertTo-BunCompletionResults -Items $items.ToArray() -WordToComplete $effectiveWordToComplete
}

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

    Complete-Bun -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}