completers/playwright_cli_completer/playwright_cli_completer.ps1

<#
.SYNOPSIS
    Registers a native PowerShell argument completer for playwright-cli.

.DESCRIPTION
    Provides static-first completion for `playwright-cli`, `playwright-cli.cmd`,
    and `playwright-cli.ps1` using the locally installed CLI help surface.

    The completer covers:
    - top-level commands
    - command-specific options
    - inline `--option=value` completion
    - path completion for file and directory slots
    - placeholder and enum values for common browser/session/target arguments

    Dot-source this file from your PowerShell profile to enable completion.
#>


Set-StrictMode -Version Latest

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

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

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

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

function New-PlaywrightCliOptionSpec {
    param(
        [string[]]$Tokens,
        [string]$Description,
        [string]$ValueKind,
        [switch]$OptionalValue,
        [string]$CompletionText
    )

    foreach ($token in @($Tokens)) {
        [pscustomobject]@{
            Token          = $token
            Description    = $Description
            ValueKind      = $ValueKind
            OptionalValue  = [bool]$OptionalValue
            CompletionText = if ([string]::IsNullOrWhiteSpace($CompletionText)) { $token } else { $CompletionText }
        }
    }
}

function New-PlaywrightCliCommandSpec {
    param(
        [string]$Name,
        [string]$Description,
        [string[]]$Positionals,
        [object[]]$Options
    )

    [pscustomobject]@{
        Name        = $Name
        Description = $Description
        Positionals = @($Positionals)
        Options     = @($Options)
    }
}

function Get-PlaywrightCliHelpCatalog {
    # @playwright/cli ships a machine-readable catalog next to the launcher; one cached read, no
    # process spawn. Returns $null when the tool or the file is absent so the static table serves.
    $command = Get-Command -Name playwright-cli.ps1, playwright-cli.cmd, playwright-cli -ErrorAction Ignore | Select-Object -First 1
    if (-not $command -or [string]::IsNullOrWhiteSpace($command.Source)) {
        return $null
    }

    $root = Split-Path -Path $command.Source -Parent
    $candidates = @(
        (Join-Path -Path $root -ChildPath 'node_modules\@playwright\cli\node_modules\playwright-core\lib\tools\cli-client\help.json')
        (Join-Path -Path $root -ChildPath 'node_modules\playwright-core\lib\tools\cli-client\help.json')
    )

    foreach ($candidate in $candidates) {
        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
            continue
        }

        try {
            $catalog = Get-Content -LiteralPath $candidate -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable
            if ($catalog -is [System.Collections.IDictionary] -and $catalog.Contains('commands')) {
                return $catalog
            }
        } catch {
            Write-Debug -Message $_.Exception.Message
        }
    }

    $null
}

function Merge-PlaywrightCliHelpCatalog {
    param(
        [object[]]$StaticCommands,
        [System.Collections.IDictionary]$Catalog
    )

    # The catalog decides which commands exist and which flags each one takes; the static table
    # contributes value kinds and wording for the entries it knows. Static-only commands are stale.
    $staticLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($command in @($StaticCommands)) {
        $staticLookup[$command.Name] = $command
    }

    foreach ($name in @($Catalog['commands'].Keys)) {
        $entry = $Catalog['commands'][$name]
        $helpLines = @([string]$entry['help'] -split "`r?`n")
        $description = ''
        foreach ($line in @($helpLines | Select-Object -Skip 1)) {
            if ([string]::IsNullOrWhiteSpace($line)) {
                if ($description) { break }
                continue
            }
            if ($line -match '^(Arguments|Options):') {
                break
            }
            $description = ($description + ' ' + $line.Trim()).Trim()
        }
        if ($description -and -not $description.EndsWith('.')) {
            $description += '.'
        }

        $flagDescriptions = @{}
        foreach ($line in $helpLines) {
            if ($line -match '^\s+--(?<flag>[A-Za-z0-9-]+)\s+(?<text>\S.*)$') {
                $flagDescriptions[$matches.flag] = $matches.text.Trim()
            }
        }

        $static = if ($staticLookup.ContainsKey($name)) { $staticLookup[$name] } else { $null }
        $options = [System.Collections.Generic.List[object]]::new()
        if ($static) {
            foreach ($option in @($static.Options)) {
                [void]$options.Add($option)
            }
        }

        $flags = $entry['flags']
        if ($flags -is [System.Collections.IDictionary]) {
            foreach ($flag in @($flags.Keys)) {
                $token = "--$flag"
                if (Find-PlaywrightCliOptionSpec -Token $token -Options $options.ToArray()) {
                    continue
                }

                $flagDescription = if ($flagDescriptions.ContainsKey($flag)) { $flagDescriptions[$flag] } else { "Option $token." }
                $flagDescription = $flagDescription.Substring(0, 1).ToUpperInvariant() + $flagDescription.Substring(1)
                if ([string]$flags[$flag] -eq 'boolean') {
                    [void]$options.Add((New-PlaywrightCliOptionSpec -Tokens @($token) -Description $flagDescription))
                } else {
                    [void]$options.Add((New-PlaywrightCliOptionSpec -Tokens @($token) -Description $flagDescription -ValueKind 'Value'))
                }
            }
        }

        $positionals = if ($static) {
            @($static.Positionals)
        } else {
            @(@($entry['args']) | ForEach-Object { 'Value' })
        }

        $commandDescription = if ($static -and $static.Description) { $static.Description } elseif ($description) { $description } else { "playwright-cli $name." }
        New-PlaywrightCliCommandSpec -Name $name -Description $commandDescription -Positionals $positionals -Options $options.ToArray()
    }
}

function Get-PlaywrightCliMetadata {
    if (Get-Variable -Name PlaywrightCliMetadata -Scope Script -ErrorAction Ignore) {
        return $script:PlaywrightCliMetadata
    }

    $commands = @(
        New-PlaywrightCliCommandSpec -Name 'open' -Description 'Open the browser.' -Positionals @('Url') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--browser') -Description 'Browser or chrome channel to use.' -ValueKind 'Browser'
            New-PlaywrightCliOptionSpec -Tokens @('--config') -Description 'Path to the configuration file.' -ValueKind 'FilePath'
            New-PlaywrightCliOptionSpec -Tokens @('--device') -Description 'Emulate a specific device, for example "iPhone 15".' -ValueKind 'DeviceName'
            New-PlaywrightCliOptionSpec -Tokens @('--headed') -Description 'Run browser in headed mode.'
            New-PlaywrightCliOptionSpec -Tokens @('--mobile') -Description 'Emulate a generic mobile device.'
            New-PlaywrightCliOptionSpec -Tokens @('--persistent') -Description 'Use a persistent browser profile.'
            New-PlaywrightCliOptionSpec -Tokens @('--profile') -Description 'Store the persistent profile in the specified directory.' -ValueKind 'DirectoryPath'
        )
        New-PlaywrightCliCommandSpec -Name 'attach' -Description 'Attach to a running Playwright browser.' -Positionals @('SessionTarget') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--cdp') -Description 'Connect to an existing browser via CDP endpoint URL.' -ValueKind 'Url'
            New-PlaywrightCliOptionSpec -Tokens @('--endpoint') -Description 'Playwright browser server endpoint to attach to.' -ValueKind 'Url'
            New-PlaywrightCliOptionSpec -Tokens @('--extension') -Description 'Connect to a browser extension, optionally specifying a browser name.' -ValueKind 'Browser' -OptionalValue
            New-PlaywrightCliOptionSpec -Tokens @('--config') -Description 'Path to the configuration file.' -ValueKind 'FilePath'
            New-PlaywrightCliOptionSpec -Tokens @('--session') -Description 'Session name.' -ValueKind 'SessionName'
        )
        New-PlaywrightCliCommandSpec -Name 'close' -Description 'Close the browser.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'detach' -Description 'Detach from an attached browser.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'goto' -Description 'Navigate to a URL.' -Positionals @('Url') -Options @()
        New-PlaywrightCliCommandSpec -Name 'type' -Description 'Type text into the active editable element.' -Positionals @('Text') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--submit') -Description 'Press Enter after typing the text.'
        )
        New-PlaywrightCliCommandSpec -Name 'click' -Description 'Perform click on a web page.' -Positionals @('Target', 'MouseButton') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--modifiers') -Description 'Modifier keys to press.' -ValueKind 'ModifierKeys'
        )
        New-PlaywrightCliCommandSpec -Name 'dblclick' -Description 'Perform double click on a web page.' -Positionals @('Target', 'MouseButton') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--modifiers') -Description 'Modifier keys to press.' -ValueKind 'ModifierKeys'
        )
        New-PlaywrightCliCommandSpec -Name 'fill' -Description 'Fill text into an editable element.' -Positionals @('Target', 'Text') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--submit') -Description 'Press Enter after filling text.'
        )
        New-PlaywrightCliCommandSpec -Name 'drag' -Description 'Perform drag and drop between two elements.' -Positionals @('Target', 'Target') -Options @()
        New-PlaywrightCliCommandSpec -Name 'drop' -Description 'Drop files or data onto an element.' -Positionals @('Target') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--path') -Description 'Absolute path to a file to drop onto the element (repeatable).' -ValueKind 'FilePath'
            New-PlaywrightCliOptionSpec -Tokens @('--data') -Description 'Data to drop in "mime/type=value" format (repeatable).' -ValueKind 'DropData'
        )
        New-PlaywrightCliCommandSpec -Name 'hover' -Description 'Hover over an element on the page.' -Positionals @('Target') -Options @()
        New-PlaywrightCliCommandSpec -Name 'select' -Description 'Select an option in a dropdown.' -Positionals @('Target', 'DropdownValue') -Options @()
        New-PlaywrightCliCommandSpec -Name 'upload' -Description 'Upload one or more files.' -Positionals @('FilePath') -Options @()
        New-PlaywrightCliCommandSpec -Name 'check' -Description 'Check a checkbox or radio button.' -Positionals @('Target') -Options @()
        New-PlaywrightCliCommandSpec -Name 'uncheck' -Description 'Uncheck a checkbox or radio button.' -Positionals @('Target') -Options @()
        New-PlaywrightCliCommandSpec -Name 'snapshot' -Description 'Capture page snapshot to obtain element references.' -Positionals @('Target') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save snapshot to a markdown file.' -ValueKind 'FilePath'
            New-PlaywrightCliOptionSpec -Tokens @('--depth') -Description 'Limit snapshot depth.' -ValueKind 'Number'
            New-PlaywrightCliOptionSpec -Tokens @('--boxes') -Description 'Include each element''s bounding box in the snapshot.'
        )
        New-PlaywrightCliCommandSpec -Name 'find' -Description 'Search the page snapshot for text or a regexp.' -Positionals @('Text') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--regex') -Description 'Regular expression to search for instead of plain text.' -ValueKind 'RegexFilter'
        )
        New-PlaywrightCliCommandSpec -Name 'eval' -Description 'Evaluate JavaScript expression on the page or an element.' -Positionals @('JavascriptExpression', 'Target') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save evaluation result to a file.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'dialog-accept' -Description 'Accept a dialog.' -Positionals @('PromptText') -Options @()
        New-PlaywrightCliCommandSpec -Name 'dialog-dismiss' -Description 'Dismiss a dialog.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'resize' -Description 'Resize the browser window.' -Positionals @('Number', 'Number') -Options @()
        New-PlaywrightCliCommandSpec -Name 'delete-data' -Description 'Delete session data.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'go-back' -Description 'Go back to the previous page.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'go-forward' -Description 'Go forward to the next page.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'reload' -Description 'Reload the current page.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'press' -Description 'Press a keyboard key.' -Positionals @('KeyboardKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'keydown' -Description 'Press a keyboard key down.' -Positionals @('KeyboardKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'keyup' -Description 'Release a keyboard key.' -Positionals @('KeyboardKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'mousemove' -Description 'Move mouse to a given position.' -Positionals @('Number', 'Number') -Options @()
        New-PlaywrightCliCommandSpec -Name 'mousedown' -Description 'Press mouse down.' -Positionals @('MouseButton') -Options @()
        New-PlaywrightCliCommandSpec -Name 'mouseup' -Description 'Press mouse up.' -Positionals @('MouseButton') -Options @()
        New-PlaywrightCliCommandSpec -Name 'mousewheel' -Description 'Scroll mouse wheel.' -Positionals @('Number', 'Number') -Options @()
        New-PlaywrightCliCommandSpec -Name 'screenshot' -Description 'Capture a screenshot of the current page or an element.' -Positionals @('Target') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'File name to save the screenshot to.' -ValueKind 'FilePath'
            New-PlaywrightCliOptionSpec -Tokens @('--type') -Description 'Image format; inferred from the filename extension when unset.' -ValueKind 'ImageFormat'
            New-PlaywrightCliOptionSpec -Tokens @('--full-page') -Description 'Capture the full scrollable page.'
            New-PlaywrightCliOptionSpec -Tokens @('--hires') -Description 'Capture a high-resolution screenshot using device pixels.'
        )
        New-PlaywrightCliCommandSpec -Name 'pdf' -Description 'Save the page as PDF.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'File name to save the PDF to.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'tab-list' -Description 'List all tabs.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'tab-new' -Description 'Create a new tab.' -Positionals @('Url') -Options @()
        New-PlaywrightCliCommandSpec -Name 'tab-close' -Description 'Close a browser tab.' -Positionals @('Number') -Options @()
        New-PlaywrightCliCommandSpec -Name 'tab-select' -Description 'Select a browser tab.' -Positionals @('Number') -Options @()
        New-PlaywrightCliCommandSpec -Name 'state-load' -Description 'Load browser storage state from a file.' -Positionals @('FilePath') -Options @()
        New-PlaywrightCliCommandSpec -Name 'state-save' -Description 'Save browser storage state to a file.' -Positionals @('FilePath') -Options @()
        New-PlaywrightCliCommandSpec -Name 'cookie-list' -Description 'List all cookies.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--domain') -Description 'Filter cookies by domain.' -ValueKind 'Domain'
            New-PlaywrightCliOptionSpec -Tokens @('--path') -Description 'Filter cookies by path.' -ValueKind 'CookiePath'
        )
        New-PlaywrightCliCommandSpec -Name 'cookie-get' -Description 'Get a cookie by name.' -Positionals @('CookieName') -Options @()
        New-PlaywrightCliCommandSpec -Name 'cookie-set' -Description 'Set a cookie with optional flags.' -Positionals @('CookieName', 'CookieValue') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--domain') -Description 'Cookie domain.' -ValueKind 'Domain'
            New-PlaywrightCliOptionSpec -Tokens @('--path') -Description 'Cookie path.' -ValueKind 'CookiePath'
            New-PlaywrightCliOptionSpec -Tokens @('--expires') -Description 'Cookie expiration as unix timestamp.' -ValueKind 'UnixTimestamp'
            New-PlaywrightCliOptionSpec -Tokens @('--httpOnly') -Description 'Set the cookie as HTTP-only.'
            New-PlaywrightCliOptionSpec -Tokens @('--secure') -Description 'Set the cookie as secure.'
            New-PlaywrightCliOptionSpec -Tokens @('--sameSite') -Description 'Cookie SameSite attribute.' -ValueKind 'CookieSameSite'
        )
        New-PlaywrightCliCommandSpec -Name 'cookie-delete' -Description 'Delete a specific cookie.' -Positionals @('CookieName') -Options @()
        New-PlaywrightCliCommandSpec -Name 'cookie-clear' -Description 'Clear all cookies.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'localstorage-list' -Description 'List all localStorage key-value pairs.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'localstorage-get' -Description 'Get a localStorage item by key.' -Positionals @('StorageKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'localstorage-set' -Description 'Set a localStorage item.' -Positionals @('StorageKey', 'StorageValue') -Options @()
        New-PlaywrightCliCommandSpec -Name 'localstorage-delete' -Description 'Delete a localStorage item.' -Positionals @('StorageKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'localstorage-clear' -Description 'Clear all localStorage.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'sessionstorage-list' -Description 'List all sessionStorage key-value pairs.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'sessionstorage-get' -Description 'Get a sessionStorage item by key.' -Positionals @('StorageKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'sessionstorage-set' -Description 'Set a sessionStorage item.' -Positionals @('StorageKey', 'StorageValue') -Options @()
        New-PlaywrightCliCommandSpec -Name 'sessionstorage-delete' -Description 'Delete a sessionStorage item.' -Positionals @('StorageKey') -Options @()
        New-PlaywrightCliCommandSpec -Name 'sessionstorage-clear' -Description 'Clear all sessionStorage.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'set-color-scheme' -Description 'Emulate the light or dark color scheme.' -Positionals @('ColorScheme') -Options @()
        New-PlaywrightCliCommandSpec -Name 'set-reduced-motion' -Description 'Emulate the reduced motion preference.' -Positionals @('ReducedMotion') -Options @()
        New-PlaywrightCliCommandSpec -Name 'set-forced-colors' -Description 'Emulate forced colors mode.' -Positionals @('ForcedColors') -Options @()
        New-PlaywrightCliCommandSpec -Name 'set-contrast' -Description 'Emulate the preferred contrast.' -Positionals @('Contrast') -Options @()
        New-PlaywrightCliCommandSpec -Name 'set-media' -Description 'Emulate the CSS media type.' -Positionals @('MediaType') -Options @()
        New-PlaywrightCliCommandSpec -Name 'clear-color-scheme' -Description 'Clear color scheme emulation.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'clear-reduced-motion' -Description 'Clear reduced motion emulation.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'clear-forced-colors' -Description 'Clear forced colors emulation.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'clear-contrast' -Description 'Clear contrast emulation.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'clear-media' -Description 'Clear CSS media type emulation.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'route' -Description 'Mock network requests matching a URL pattern.' -Positionals @('RoutePattern') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--status') -Description 'HTTP status code.' -ValueKind 'Number'
            New-PlaywrightCliOptionSpec -Tokens @('--body') -Description 'Response body text or JSON string.' -ValueKind 'ResponseBody'
            New-PlaywrightCliOptionSpec -Tokens @('--content-type') -Description 'Content-Type header.' -ValueKind 'ContentType'
            New-PlaywrightCliOptionSpec -Tokens @('--header') -Description 'Header in "name: value" format.' -ValueKind 'Header'
            New-PlaywrightCliOptionSpec -Tokens @('--remove-header') -Description 'Comma-separated header names to remove.' -ValueKind 'HeaderNameList'
        )
        New-PlaywrightCliCommandSpec -Name 'route-list' -Description 'List all active network routes.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'unroute' -Description 'Remove routes matching a pattern.' -Positionals @('RoutePattern') -Options @()
        New-PlaywrightCliCommandSpec -Name 'requests' -Description 'List all network requests since loading the page, numbered for the request command.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--static') -Description 'Include successful static resources such as images, fonts and scripts.'
            New-PlaywrightCliOptionSpec -Tokens @('--filter') -Description 'Only return requests whose URL matches this regexp.' -ValueKind 'RegexFilter'
            New-PlaywrightCliOptionSpec -Tokens @('--clear') -Description 'Clear the network list.'
        )
        New-PlaywrightCliCommandSpec -Name 'request' -Description 'Show full details of a single network request by its number.' -Positionals @('RequestIndex') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save the result to a file instead of returning it as text.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'request-headers' -Description 'Print only the request headers of a single network request.' -Positionals @('RequestIndex') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save the result to a file instead of returning it as text.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'request-body' -Description 'Print only the request body of a single network request.' -Positionals @('RequestIndex') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save the result to a file instead of returning it as text.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'response-headers' -Description 'Print only the response headers of a single network request.' -Positionals @('RequestIndex') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save the result to a file instead of returning it as text.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'response-body' -Description 'Print the response body of a single network request.' -Positionals @('RequestIndex') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Save the result to a file instead of returning it as text.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'network-state-set' -Description 'Set the browser network state to online or offline.' -Positionals @('NetworkState') -Options @()
        New-PlaywrightCliCommandSpec -Name 'console' -Description 'List console messages.' -Positionals @('ConsoleLevel') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--clear') -Description 'Clear the console list.'
        )
        New-PlaywrightCliCommandSpec -Name 'run-code' -Description 'Run a Playwright code snippet.' -Positionals @('PlaywrightCode') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--filename') -Description 'Load code from the specified file.' -ValueKind 'FilePath'
        )
        New-PlaywrightCliCommandSpec -Name 'recording-start' -Description 'Start recording user actions.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'recording-stop' -Description 'Stop recording user actions and print them as Playwright code.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'tracing-start' -Description 'Start trace recording.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'tracing-stop' -Description 'Stop trace recording.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'video-start' -Description 'Start video recording.' -Positionals @('FilePath') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--size') -Description 'Video frame size.' -ValueKind 'VideoSize'
        )
        New-PlaywrightCliCommandSpec -Name 'video-stop' -Description 'Stop video recording.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'video-chapter' -Description 'Add a chapter marker to the video recording.' -Positionals @('ChapterTitle') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--description') -Description 'Chapter description.' -ValueKind 'ChapterDescription'
            New-PlaywrightCliOptionSpec -Tokens @('--duration') -Description 'Duration in milliseconds for the chapter card.' -ValueKind 'Number'
        )
        New-PlaywrightCliCommandSpec -Name 'video-show-actions' -Description 'Annotate subsequent actions on the page with a callout and target highlight.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--duration') -Description 'How long each action annotation stays on screen, in milliseconds.' -ValueKind 'Number'
            New-PlaywrightCliOptionSpec -Tokens @('--position') -Description 'Where to place the action title.' -ValueKind 'ActionPosition'
            New-PlaywrightCliOptionSpec -Tokens @('--cursor') -Description 'Cursor decoration between action points.' -ValueKind 'CursorStyle'
        )
        New-PlaywrightCliCommandSpec -Name 'video-hide-actions' -Description 'Stop annotating actions performed on the page.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'show' -Description 'Show the Playwright dashboard.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--port') -Description 'Start as a blocking HTTP server on this port (0 picks a random port).' -ValueKind 'Number'
            New-PlaywrightCliOptionSpec -Tokens @('--host') -Description 'Host to bind to when using --port.' -ValueKind 'HostName'
            New-PlaywrightCliOptionSpec -Tokens @('--annotate') -Description 'Switch the dashboard into annotation mode.'
            New-PlaywrightCliOptionSpec -Tokens @('--kill') -Description 'Kill the dashboard daemon.'
        )
        New-PlaywrightCliCommandSpec -Name 'pause-at' -Description 'Run the test to a location and pause there.' -Positionals @('SourceLocation') -Options @()
        New-PlaywrightCliCommandSpec -Name 'resume' -Description 'Resume the test execution.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'step-over' -Description 'Step over the next call in the test.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'generate-locator' -Description 'Generate a Playwright locator for the given element.' -Positionals @('Target') -Options @()
        New-PlaywrightCliCommandSpec -Name 'highlight' -Description 'Show (or with --hide, remove) a highlight overlay for an element.' -Positionals @('Target') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--hide') -Description 'Hide the highlight for this element, or all highlights when no element is given.'
            New-PlaywrightCliOptionSpec -Tokens @('--style') -Description 'Additional inline CSS applied to the highlight overlay.' -ValueKind 'CssStyle'
        )
        New-PlaywrightCliCommandSpec -Name 'config-print' -Description 'Print the effective configuration.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'install' -Description 'Initialize the workspace.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--skills') -Description 'Install skills.' -ValueKind 'SkillSet'
            New-PlaywrightCliOptionSpec -Tokens @('--global') -Description 'Install skills into the home directory instead of the workspace (requires --skills).'
        )
        New-PlaywrightCliCommandSpec -Name 'install-browser' -Description 'Install browser binaries.' -Positionals @('InstallBrowser') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--with-deps') -Description 'Install system dependencies for browsers.'
            New-PlaywrightCliOptionSpec -Tokens @('--dry-run') -Description 'Print information without executing installation.'
            New-PlaywrightCliOptionSpec -Tokens @('--list') -Description 'Print list of browsers from all Playwright installations.'
            New-PlaywrightCliOptionSpec -Tokens @('--force') -Description 'Force reinstall of already installed browsers.'
            New-PlaywrightCliOptionSpec -Tokens @('--only-shell') -Description 'Only install the headless shell when installing Chromium.'
            New-PlaywrightCliOptionSpec -Tokens @('--no-shell') -Description 'Do not install Chromium headless shell.'
        )
        New-PlaywrightCliCommandSpec -Name 'list' -Description 'List browser sessions.' -Positionals @() -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--all') -Description 'List browser sessions across all workspaces.'
        )
        New-PlaywrightCliCommandSpec -Name 'close-all' -Description 'Close all browser sessions.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'kill-all' -Description 'Forcefully kill all browser sessions.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'webmcp-list' -Description 'List the WebMCP tools registered by the page.' -Positionals @() -Options @()
        New-PlaywrightCliCommandSpec -Name 'webmcp-call' -Description 'Call a WebMCP tool registered by the page.' -Positionals @('WebMcpToolName') -Options @(
            New-PlaywrightCliOptionSpec -Tokens @('--params') -Description 'Tool input parameters as a JSON object.' -ValueKind 'JsonObject'
            New-PlaywrightCliOptionSpec -Tokens @('--frame') -Description 'Frame that registered the tool, as reported by webmcp-list.' -ValueKind 'FrameName'
        )
        New-PlaywrightCliCommandSpec -Name 'tray' -Description 'Run the Playwright tray application.' -Positionals @() -Options @()
    )

    $catalog = Get-PlaywrightCliHelpCatalog
    if ($catalog) {
        $commands = @(Merge-PlaywrightCliHelpCatalog -StaticCommands $commands -Catalog $catalog)
    }

    $commandLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($command in $commands) {
        $commandLookup[$command.Name] = $command
    }

    $script:PlaywrightCliMetadata = @{
        Commands             = $commands
        CommandLookup        = $commandLookup
        GlobalOptions        = @(
            New-PlaywrightCliOptionSpec -Tokens @('--help') -Description 'Print help for a command.' -ValueKind 'CommandName' -OptionalValue
            New-PlaywrightCliOptionSpec -Tokens @('--json') -Description 'Output the response as JSON.'
            New-PlaywrightCliOptionSpec -Tokens @('--raw') -Description 'Output only the result value, without status and code.'
            New-PlaywrightCliOptionSpec -Tokens @('--version') -Description 'Print version.'
            New-PlaywrightCliOptionSpec -Tokens @('-s') -Description 'Session name for the command invocation.' -ValueKind 'SessionName' -CompletionText '-s='
        )
        BrowserValues        = @('chrome', 'firefox', 'webkit', 'msedge')
        InstallBrowserValues = @(
            'chromium', 'chromium-headless-shell', 'chrome', 'chrome-beta', 'chrome-dev', 'chrome-canary',
            'firefox', 'webkit', 'msedge', 'msedge-beta', 'msedge-dev', 'msedge-canary'
        )
        MouseButtons         = @('left', 'right', 'middle')
        ModifierKeys         = @('Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift')
        ImageFormats         = @('png', 'jpeg', 'webp')
        ActionPositions      = @('top-left', 'top', 'top-right', 'bottom-left', 'bottom', 'bottom-right')
        CursorStyles         = @('pointer', 'none')
        KeyboardKeys         = @(
            'Enter', 'Tab', 'Escape', 'Space', 'Backspace', 'Delete', 'Insert',
            'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
            'Home', 'End', 'PageUp', 'PageDown',
            'Control', 'Alt', 'Shift', 'Meta',
            'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
        )
        NetworkStates        = @('online', 'offline')
        CookieSameSiteValues = @('Strict', 'Lax', 'None')
        SkillValues          = @('claude', 'agents')
        VideoSizes           = @('800x600', '1024x768', '1280x720', '1600x900', '1920x1080')
        ConsoleLevels        = @('debug', 'info', 'warning', 'warn', 'error')
        ContentTypes         = @('application/json', 'text/plain', 'text/html')
        ColorSchemes         = @('light', 'dark', 'no-preference')
        ReducedMotionValues  = @('reduce', 'no-preference')
        ForcedColorsValues   = @('active', 'none')
        ContrastValues       = @('more', 'no-preference')
        MediaTypes           = @('screen', 'print')
    }

    $script:PlaywrightCliMetadata
}

function Get-PlaywrightCliTokenText {
    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-PlaywrightCliProcessedTokens {
    param(
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [string]$WordToComplete,
        [int]$CursorPosition
    )

    if ($CommandAst.CommandElements.Count -le 1) {
        return @()
    }

    # Only the elements left of the cursor have been "processed"; the element that contains the
    # cursor is the word being completed and anything right of it is ignored. Extent offsets and
    # $CursorPosition are both absolute within the input line.
    $tokens = New-Object System.Collections.Generic.List[string]
    foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) {
        if ($null -eq $element -or $element.Extent.StartOffset -ge $CursorPosition) {
            break
        }

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

        [void]$tokens.Add((Get-PlaywrightCliTokenText -Element $element))
    }

    # A word PowerShell hands over that still sits at the end of the list (an empty cursor
    # position, or a token that ends exactly at the cursor) is the current word, not a processed one.
    if ($tokens.Count -gt 0 -and -not [string]::IsNullOrEmpty($WordToComplete) -and $tokens[$tokens.Count - 1] -eq $WordToComplete) {
        $tokens.RemoveAt($tokens.Count - 1)
    }

    @($tokens.ToArray())
}

function Get-PlaywrightCliCommandSpec {
    param([string]$CommandName)

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

    $metadata = Get-PlaywrightCliMetadata
    if ($metadata.CommandLookup.ContainsKey($CommandName)) {
        return $metadata.CommandLookup[$CommandName]
    }

    $null
}

function Find-PlaywrightCliOptionSpec {
    param(
        [string]$Token,
        [object[]]$Options
    )

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

    $normalizedToken = if ($Token.Contains('=')) {
        $Token.Substring(0, $Token.IndexOf('='))
    } else {
        $Token
    }

    foreach ($option in @($Options)) {
        if ($option.Token.TrimEnd('=').Equals($normalizedToken, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $option
        }
    }

    $null
}

function ConvertFrom-PlaywrightCliTypedWord {
    # 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 ($Value -notmatch '^[''"\u2018-\u201E]') {
        return $Value
    }

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

function ConvertTo-PlaywrightCliQuotedValue {
    # 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 ([string]::IsNullOrEmpty($Value)) {
        return $Value
    }

    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-PlaywrightCliPathCompletions {
    param(
        [string]$PathPrefix,
        [switch]$DirectoriesOnly,
        [string]$CompletionPrefix = ''
    )

    $quoteChar = if ($PathPrefix -match '^[''"\u2018-\u201E]') { $PathPrefix.Substring(0, 1) } else { '' }
    $items = [System.Management.Automation.CompletionCompleters]::CompleteFilename((ConvertFrom-PlaywrightCliTypedWord -Value $PathPrefix))
    foreach ($item in @($items)) {
        # CompleteFilename already quotes a path with spaces, so Test-Path on CompletionText would
        # drop every such directory; its ResultType says whether the entry is a container.
        if ($DirectoriesOnly -and $item.ResultType -ne [System.Management.Automation.CompletionResultType]::ProviderContainer) {
            continue
        }

        # CompleteFilename quotes for PowerShell and wildcard-escapes for -Path parameters
        # (tick``x.txt), so unwrap it with the parser and unescape before quoting once here.
        $path = $item.CompletionText
        if ($path -match '^[''"\u2018-\u201E]') {
            $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
            }
        }

        $path = [System.Management.Automation.WildcardPattern]::Unescape($path)
        New-PlaywrightCliCompletionResult `
            -CompletionText "$CompletionPrefix$(ConvertTo-PlaywrightCliQuotedValue -Value $path -QuoteChar $quoteChar)" `
            -ListItemText $item.ListItemText `
            -ResultType $item.ResultType `
            -ToolTip $item.ToolTip
    }
}

function Get-PlaywrightCliValueCompletions {
    param(
        [string]$ValueKind,
        [string]$WordToComplete,
        [string]$ContextToken,
        [string]$InlinePrefix,
        [string]$TypedValue = $WordToComplete
    )

    $metadata = Get-PlaywrightCliMetadata
    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]

    $addResult = {
        param(
            [string]$completionText,
            [string]$toolTip,
            [string]$resultType = 'ParameterValue',
            [string]$listItemText = $completionText
        )

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

        if ($completionText -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            return
        }

        $finalCompletion = if ([string]::IsNullOrEmpty($InlinePrefix)) {
            $completionText
        } else {
            "$InlinePrefix$completionText"
        }

        $finalListItemText = if ([string]::IsNullOrEmpty($InlinePrefix)) {
            $listItemText
        } else {
            $completionText
        }

        [void]$results.Add(
            (New-PlaywrightCliCompletionResult -CompletionText $finalCompletion -ListItemText $finalListItemText -ResultType $resultType -ToolTip $toolTip)
        )
    }

    switch ($ValueKind) {
        'CommandName' {
            foreach ($command in $metadata.Commands) {
                & $addResult $command.Name $command.Description
            }
        }
        'Browser' {
            foreach ($value in $metadata.BrowserValues) {
                & $addResult $value "Browser value for $ContextToken"
            }
        }
        'InstallBrowser' {
            foreach ($value in $metadata.InstallBrowserValues) {
                & $addResult $value 'Browser to install'
            }
        }
        'MouseButton' {
            foreach ($value in $metadata.MouseButtons) {
                & $addResult $value "Mouse button for $ContextToken"
            }
        }
        'ModifierKeys' {
            $prefix = ''
            $valuePrefix = $WordToComplete
            if ($WordToComplete -like '*,*') {
                $lastComma = $WordToComplete.LastIndexOf(',')
                $prefix = $WordToComplete.Substring(0, $lastComma + 1)
                $valuePrefix = $WordToComplete.Substring($lastComma + 1)
            }

            foreach ($value in $metadata.ModifierKeys) {
                if ($value -notlike ([System.Management.Automation.WildcardPattern]::Escape($valuePrefix) + '*')) {
                    continue
                }

                & $addResult "$prefix$value" 'Modifier key'
            }
        }
        'KeyboardKey' {
            foreach ($value in $metadata.KeyboardKeys) {
                & $addResult $value 'Keyboard key'
            }

            & $addResult '<key>' "Key value for $ContextToken"
        }
        'NetworkState' {
            foreach ($value in $metadata.NetworkStates) {
                & $addResult $value 'Network state'
            }
        }
        'CookieSameSite' {
            foreach ($value in $metadata.CookieSameSiteValues) {
                & $addResult $value 'SameSite value'
            }
        }
        'SkillSet' {
            foreach ($value in $metadata.SkillValues) {
                & $addResult $value 'Skill set to install'
            }
        }
        'VideoSize' {
            foreach ($value in $metadata.VideoSizes) {
                & $addResult $value 'Video frame size'
            }
        }
        'ConsoleLevel' {
            foreach ($value in $metadata.ConsoleLevels) {
                & $addResult $value 'Minimum console level'
            }
        }
        'ImageFormat' {
            foreach ($value in $metadata.ImageFormats) {
                & $addResult $value 'Screenshot image format'
            }
        }
        'ActionPosition' {
            foreach ($value in $metadata.ActionPositions) {
                & $addResult $value 'Action title position'
            }
        }
        'CursorStyle' {
            foreach ($value in $metadata.CursorStyles) {
                & $addResult $value 'Cursor decoration'
            }
        }
        'RequestIndex' {
            & $addResult '<index>' '1-based request number as listed by requests'
        }
        'DeviceName' {
            & $addResult '<device>' 'Device to emulate, for example "iPhone 15"'
        }
        'HostName' {
            & $addResult 'localhost' 'Host to bind to'
            & $addResult '<host>' 'Host to bind to'
        }
        'DropData' {
            & $addResult '<mime/type=value>' 'Data to drop, for example text/plain=hello'
        }
        'CssStyle' {
            & $addResult '<css>' 'Inline CSS, for example "outline: 2px dashed red"'
        }
        'ContentType' {
            foreach ($value in $metadata.ContentTypes) {
                & $addResult $value 'Content-Type value'
            }
        }
        'FilePath' {
            foreach ($item in @(Get-PlaywrightCliPathCompletions -PathPrefix $TypedValue -CompletionPrefix $InlinePrefix)) {
                [void]$results.Add($item)
            }
        }
        'DirectoryPath' {
            foreach ($item in @(Get-PlaywrightCliPathCompletions -PathPrefix $TypedValue -DirectoriesOnly -CompletionPrefix $InlinePrefix)) {
                [void]$results.Add($item)
            }
        }
        'Url' {
            & $addResult 'https://' "URL value for $ContextToken"
            & $addResult 'http://' "URL value for $ContextToken"
        }
        'SessionName' {
            & $addResult '<session>' "Session name for $ContextToken"
        }
        'SessionTarget' {
            & $addResult '<name>' 'Bound browser name to attach to'
        }
        'Target' {
            & $addResult '<target>' 'Exact element reference or selector'
        }
        'Text' {
            & $addResult '<text>' "Text value for $ContextToken"
        }
        'DropdownValue' {
            & $addResult '<value>' 'Dropdown value'
        }
        'PromptText' {
            & $addResult '<prompt>' 'Prompt text for the dialog'
        }
        'CookieName' {
            & $addResult '<name>' 'Cookie name'
        }
        'CookieValue' {
            & $addResult '<value>' 'Cookie value'
        }
        'StorageKey' {
            & $addResult '<key>' 'Storage key'
        }
        'StorageValue' {
            & $addResult '<value>' 'Storage value'
        }
        'RoutePattern' {
            & $addResult '**/api/*' 'URL pattern such as **/api/*'
            & $addResult '<pattern>' 'URL pattern to match'
        }
        'ResponseBody' {
            & $addResult '<body>' 'Response body text or JSON string'
        }
        'Header' {
            & $addResult '<name: value>' 'Header in "name: value" format'
        }
        'HeaderNameList' {
            & $addResult '<name1,name2>' 'Comma-separated header names'
        }
        'Domain' {
            & $addResult '<domain>' 'Domain value'
        }
        'CookiePath' {
            & $addResult '/' 'Cookie path'
            & $addResult '<path>' 'Cookie path value'
        }
        'JavascriptExpression' {
            & $addResult '() => { }' 'JavaScript expression to evaluate'
            & $addResult '(element) => { }' 'JavaScript expression that receives the element'
        }
        'PlaywrightCode' {
            & $addResult 'async page => { }' 'Playwright code snippet'
            & $addResult '<code>' 'Playwright code snippet'
        }
        'Number' {
            & $addResult '<n>' "Numeric value for $ContextToken"
        }
        'UnixTimestamp' {
            & $addResult '<unix-timestamp>' 'Unix timestamp value'
        }
        'RegexFilter' {
            & $addResult '/api/.*' 'Regular expression filter'
            & $addResult '<regex>' 'Regular expression filter'
        }
        'ChapterTitle' {
            & $addResult '<title>' 'Chapter title'
        }
        'ChapterDescription' {
            & $addResult '<description>' 'Chapter description'
        }
        'ColorScheme' {
            foreach ($value in $metadata.ColorSchemes) {
                & $addResult $value 'Color scheme to emulate'
            }
        }
        'ReducedMotion' {
            foreach ($value in $metadata.ReducedMotionValues) {
                & $addResult $value 'Reduced motion preference to emulate'
            }
        }
        'ForcedColors' {
            foreach ($value in $metadata.ForcedColorsValues) {
                & $addResult $value 'Forced colors mode to emulate'
            }
        }
        'Contrast' {
            foreach ($value in $metadata.ContrastValues) {
                & $addResult $value 'Contrast preference to emulate'
            }
        }
        'MediaType' {
            foreach ($value in $metadata.MediaTypes) {
                & $addResult $value 'CSS media type to emulate'
            }
        }
        'WebMcpToolName' {
            & $addResult '<name>' 'Name of the WebMCP tool to call'
        }
        'JsonObject' {
            & $addResult '<json>' "JSON object for $ContextToken"
        }
        'FrameName' {
            & $addResult '<frame>' 'Frame that registered the tool'
        }
        'SourceLocation' {
            & $addResult '<file>:<line>' 'Source location such as example.spec.ts:42'
        }
        default {
            & $addResult '<value>' "Value for $ContextToken"
        }
    }

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($item in @($results)) {
        if ($seen.Add($item.CompletionText)) {
            $item
        }
    }
}

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

    foreach ($option in @($Options)) {
        $completionText = $option.CompletionText
        if ($completionText -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            continue
        }

        $toolTip = if ($option.ValueKind) {
            "$($option.Token): $($option.Description)"
        } else {
            $option.Description
        }

        New-PlaywrightCliCompletionResult -CompletionText $completionText -ResultType 'ParameterName' -ToolTip $toolTip
    }
}

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

    $metadata = Get-PlaywrightCliMetadata
    $tokens = @(Get-PlaywrightCliProcessedTokens -CommandAst $CommandAst -WordToComplete $WordToComplete -CursorPosition $CursorPosition)

    # PowerShell hands a quoted word over re-quoted ("'it" arrives as "'it'", a typographic quote
    # as ') and drops the quote inside an attached "--opt='it", while it replaces the word as typed;
    # path candidates keep the typed quote, so they read the word from the element under the cursor.
    $typedWord = $WordToComplete
    $cursorElement = $CommandAst.CommandElements | Select-Object -Skip 1 |
        Where-Object { $_.Extent.StartOffset -lt $CursorPosition -and $_.Extent.EndOffset -ge $CursorPosition } |
        Select-Object -First 1
    if ($cursorElement -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
        $typedWord = $cursorElement.Extent.Text.Substring(0, $CursorPosition - $cursorElement.Extent.StartOffset)
    }
    $commandSpec = $null
    $positionalsConsumed = 0
    $expectingValue = $null

    foreach ($token in @($tokens)) {
        if ($expectingValue) {
            $consumeAsValue = $true

            if (-not $commandSpec -and $expectingValue.OptionalValue -and $expectingValue.Token -ne '--help') {
                if (Get-PlaywrightCliCommandSpec -CommandName $token) {
                    $consumeAsValue = $false
                }
            }

            if ($consumeAsValue) {
                $expectingValue = $null
                continue
            }

            $expectingValue = $null
        }

        if (-not $commandSpec) {
            if ($token.StartsWith('-')) {
                $globalOption = Find-PlaywrightCliOptionSpec -Token $token -Options $metadata.GlobalOptions
                if ($globalOption) {
                    if (-not $token.Contains('=') -and $globalOption.ValueKind) {
                        $expectingValue = $globalOption
                    }

                    continue
                }

                continue
            }

            $resolvedCommand = Get-PlaywrightCliCommandSpec -CommandName $token
            if ($resolvedCommand) {
                $commandSpec = $resolvedCommand
            }

            continue
        }

        if ($token.StartsWith('-')) {
            $commandOption = Find-PlaywrightCliOptionSpec -Token $token -Options $commandSpec.Options
            if ($commandOption) {
                if (-not $token.Contains('=') -and $commandOption.ValueKind) {
                    $expectingValue = $commandOption
                }

                continue
            }

            continue
        }

        $positionalsConsumed++
    }

    if ($expectingValue -and $expectingValue.OptionalValue -and $WordToComplete -like '-*') {
        $expectingValue = $null
    }

    if ([string]::IsNullOrEmpty($WordToComplete) -and $tokens.Count -gt 0 -and $tokens[-1].EndsWith('=')) {
        $options = if ($commandSpec) { @($metadata.GlobalOptions + $commandSpec.Options) } else { $metadata.GlobalOptions }
        $inlineEmptyValueOption = Find-PlaywrightCliOptionSpec -Token $tokens[-1] -Options $options
        if ($inlineEmptyValueOption -and $inlineEmptyValueOption.ValueKind) {
            Get-PlaywrightCliValueCompletions -ValueKind $inlineEmptyValueOption.ValueKind -WordToComplete '' -ContextToken $inlineEmptyValueOption.Token -InlinePrefix $tokens[-1]
            return
        }
    }

    if ($WordToComplete -like '*=*') {
        $equalsIndex = $WordToComplete.IndexOf('=')
        $flagPart = $WordToComplete.Substring(0, $equalsIndex)
        $valuePrefix = $WordToComplete.Substring($equalsIndex + 1)
        $options = if ($commandSpec) { @($metadata.GlobalOptions + $commandSpec.Options) } else { $metadata.GlobalOptions }
        $inlineOption = Find-PlaywrightCliOptionSpec -Token $flagPart -Options $options
        if ($inlineOption -and $inlineOption.ValueKind) {
            Get-PlaywrightCliValueCompletions -ValueKind $inlineOption.ValueKind -WordToComplete $valuePrefix -TypedValue $typedWord.Substring($typedWord.IndexOf('=') + 1) -ContextToken $inlineOption.Token -InlinePrefix "$flagPart="
            return
        }
    }

    if ($expectingValue) {
        Get-PlaywrightCliValueCompletions -ValueKind $expectingValue.ValueKind -WordToComplete $WordToComplete -TypedValue $typedWord -ContextToken $expectingValue.Token
        return
    }

    if ($WordToComplete -like '-*') {
        $options = if ($commandSpec) { @($metadata.GlobalOptions + $commandSpec.Options) } else { $metadata.GlobalOptions }
        Get-PlaywrightCliOptionCompletions -Options $options -WordToComplete $WordToComplete
        return
    }

    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]

    if (-not $commandSpec) {
        foreach ($command in $metadata.Commands) {
            if ($command.Name -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                [void]$results.Add(
                    (New-PlaywrightCliCompletionResult -CompletionText $command.Name -ToolTip $command.Description)
                )
            }
        }

        if ([string]::IsNullOrEmpty($WordToComplete)) {
            foreach ($option in @(Get-PlaywrightCliOptionCompletions -Options $metadata.GlobalOptions -WordToComplete '')) {
                [void]$results.Add($option)
            }
        }
    } else {
        $valueKind = if ($positionalsConsumed -lt $commandSpec.Positionals.Count) {
            $commandSpec.Positionals[$positionalsConsumed]
        } else {
            $null
        }

        if ($valueKind) {
            foreach ($item in @(Get-PlaywrightCliValueCompletions -ValueKind $valueKind -WordToComplete $WordToComplete -TypedValue $typedWord -ContextToken $commandSpec.Name)) {
                [void]$results.Add($item)
            }
        }

        if ([string]::IsNullOrEmpty($WordToComplete)) {
            foreach ($option in @(Get-PlaywrightCliOptionCompletions -Options $commandSpec.Options -WordToComplete '')) {
                [void]$results.Add($option)
            }
        }
    }

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($item in @($results)) {
        if ($seen.Add($item.CompletionText)) {
            $item
        }
    }
}

Register-ArgumentCompleter -Native -CommandName @('playwright-cli', 'playwright-cli.cmd', 'playwright-cli.ps1') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

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