private/Split-WtwOnHostArgs.ps1

function Split-WtwOnHostArgs {
    <#
    .SYNOPSIS
        Extract the target host from a raw wtw argument list.
    .DESCRIPTION
        Invoke-Wtw dispatches on $args[0], so a leading `--on` would be read as
        the subcommand. This runs first and strips the host selector wherever it
        appears, leaving a normal argument list behind.

        Two accepted forms:
          wtw --on at cursor auth explicit, position-independent
          wtw at cursor auth shorthand — only when the first token is a
                                      known host AND something follows it

        The shorthand is deliberately the most conservative: it requires an exact
        host name or alias, never a prefix, because a prefix could shadow a repo
        alias and silently send you to the wrong machine. `--on` itself does
        allow prefix resolution, since there the intent is unambiguous.

        There is deliberately no `@host` form: PowerShell's argument mode reads a
        bare `@at` as splatting of `$at`, so an undefined variable makes the token
        vanish before wtw is even called — the request would silently run locally.
    .PARAMETER ArgList
        Raw arguments as received by Invoke-Wtw.
    .PARAMETER KnownHosts
        Flat list of host names and aliases (Get-WtwHostNames). Passed in rather
        than read from config so this stays a pure function.
    .OUTPUTS
        @{ Host = <string|$null>; Args = <object[]> }
    #>

    [CmdletBinding()]
    param(
        [AllowNull()] [object[]] $ArgList,
        [AllowNull()] [string[]] $KnownHosts
    )

    $remaining = @()
    $targetHost = $null
    $items = @($ArgList)
    $i = 0

    while ($i -lt $items.Count) {
        $arg = "$($items[$i])"

        if ($arg -ieq '--on' -or $arg -ieq '-on') {
            if (($i + 1) -lt $items.Count) {
                $targetHost = "$($items[$i + 1])"
                $i += 2
                continue
            }
            # Dangling `--on` — drop it and let the caller report a missing host.
            $i++
            continue
        }

        # Comma operator, not a bare +=. PowerShell argument mode turns
        # `--alias at,troll` into ONE array-valued argument, and `+=` would
        # splice it into two tokens — the flag would take 'at' and 'troll' would
        # silently become a stray positional. This pre-scan must be transparent
        # to every argument shape it does not consume.
        $remaining += , $items[$i]
        $i++
    }

    # Leading bare host: `wtw at cursor auth`. Exact match only.
    if (-not $targetHost -and $remaining.Count -ge 2 -and $KnownHosts) {
        $first = "$($remaining[0])"
        if ($KnownHosts -contains $first) {
            $targetHost = $first
            $remaining = @($remaining[1..($remaining.Count - 1)])
        }
    }

    return @{ Host = $targetHost; Args = @($remaining) }
}

function Remove-WtwFlagWithValue {
    <#
    .SYNOPSIS
        Drop a `--flag value` pair from a raw argument list.
    .DESCRIPTION
        Local-only flags must not reach the remote CLI. `wtw --on at list --via lan`
        forwards its flags verbatim so the remote's own parser handles
        `--detailed` and friends — but `--via` is consumed here, and the remote
        `wtw list` has no such parameter, so leaving it in makes the whole command
        fail on the far side.
    .PARAMETER ArgList
        Raw tokens.
    .PARAMETER Flag
        Flag name without dashes, matched case-insensitively.
    #>

    [CmdletBinding()]
    param(
        [AllowNull()] [object[]] $ArgList,
        [Parameter(Mandatory)] [string] $Flag
    )

    $items = @($ArgList)
    $kept = @()
    $i = 0
    while ($i -lt $items.Count) {
        $token = "$($items[$i])"
        if ($token -ieq "--$Flag" -or $token -ieq "-$Flag") {
            # Skip the flag and its value, if the next token is not another flag.
            $i++
            if ($i -lt $items.Count -and "$($items[$i])" -notmatch '^-') { $i++ }
            continue
        }
        $kept += , $items[$i]
        $i++
    }
    return , @($kept)
}

# Commands that mutate the remote's own registry. They are not refused — they
# are executed ON the remote, which is what keeps its registry authoritative.
$script:WtwRemoteExecCommands = @(
    'run', 'create', 'add', 'remove', 'rm', 'delete', 'del', 'sync', 'color',
    'clean', 'init', 'workspace', 'ws', 'copy', 'unregister', 'unreg', 'install', 'update'
)

function Get-WtwRemoteCommandMode {
    <#
    .SYNOPSIS
        How a subcommand should be handled when `--on` is present.
    .DESCRIPTION
        Three outcomes:

          local interpreted here — a read whose result we render, or a VS Code
                  editor launch where the window is local and the files remote.
          exec forwarded verbatim and executed on the remote machine.
          none refused, because neither reading makes sense.

        `go` is the only hard refusal: there is no cd to another machine. The
        app-launchers (t3, cmux, claudecode, chatgpt…) are ambiguous rather than
        impossible — "register that project over there" is meaningful — so they
        are reachable through the explicit `run`, where the intent is stated
        rather than guessed.
    .OUTPUTS
        'local' | 'exec' | 'none'
    #>

    [CmdletBinding()]
    param([AllowNull()] [string] $Command)

    if (-not $Command) { return 'none' }
    if ($Command -in @('open', 'list', 'ls', 'info', 'show')) { return 'local' }
    if (Resolve-WtwEditorFamilyMember -Name $Command) { return 'local' }
    if ($Command -in $script:WtwRemoteExecCommands) { return 'exec' }
    return 'none'
}

function Test-WtwRemoteCapableCommand {
    <#
    .SYNOPSIS
        Can this subcommand be used with --on at all?
    #>

    [CmdletBinding()]
    param([AllowNull()] [string] $Command)

    return ((Get-WtwRemoteCommandMode -Command $Command) -ne 'none')
}