public/Invoke-WtwHost.ps1

function Invoke-WtwHost {
    <#
    .SYNOPSIS
        Manage the remote machines wtw can open worktrees on.
    .DESCRIPTION
        Hosts are stored in ~/.wtw/config.json under `hosts`, and mirrored into
        ~/.ssh/config.d/wtw so the editor's Remote-SSH extension — which resolves
        hosts through the ssh client, not through wtw — can see them too.

        Subcommands:
          list Show configured hosts and their ssh status
          add <name> [options] Add or update a host, then sync ssh config
          remove <name> Drop a host, then sync ssh config
          sync Re-probe addresses, rewrite ~/.ssh/config.d/wtw
          trust <name> Show host-key fingerprints, then add to known_hosts
          test <name> Probe addresses, ssh config, and the remote wtw
    .PARAMETER Action
        Subcommand (default: list).
    .PARAMETER Name
        Host name for add / remove / test.
    .PARAMETER Alias
        Short aliases, e.g. "at,troll". Typed as an array because PowerShell's
        argument mode already turns a bare `at,troll` into two arguments, while a
        quoted 'at,troll' arrives as one string — both forms are accepted.
    .PARAMETER User
        SSH user.
    .PARAMETER Address
        Ordered candidate addresses for ssh. Prefer an mDNS name
        (`snowpomme.local`) first and a last-known IP as backup: on DHCP — or
        when a machine moves between wifi and a USB adapter — the address changes
        but the .local name does not. `wtw host sync` writes whichever candidate
        answers on the ssh port.
    .PARAMETER Identity
        IdentityFile path.
    .PARAMETER Port
        SSH port.
    .PARAMETER Platform
        Remote OS: windows | linux | macos. Drives remote path translation.
    .PARAMETER Wtw
        Command used to invoke wtw on the remote (default: wtw).
    .EXAMPLE
        wtw host add arctictroll --alias at --user sno --address 192.168.3.7 --platform windows --identity ~/.ssh/id_ed25519_arctictroll
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)] [string] $Action = 'list',
        [Parameter(Position = 1)] [string] $Name,
        [string[]] $Alias,
        [string] $User,
        [string[]] $Address,
        [string] $Identity,
        [string] $Port,
        [ValidateSet('windows', 'linux', 'macos')] [string] $Platform = 'linux',
        [string] $Wtw,
        [string] $Pwsh
    )

    switch ($Action.ToLowerInvariant()) {
        'list' {
            $hosts = Get-WtwHosts
            if ($hosts.Count -eq 0) {
                Write-Host ''
                Write-Host ' No remote hosts configured.' -ForegroundColor Yellow
                Write-Host ' Add one: wtw host add arctictroll --alias at --user sno --address 192.168.3.7 --platform windows' -ForegroundColor DarkGray
                Write-Host ''
                return
            }
            Write-Host ''
            foreach ($h in ($hosts | Sort-Object { $_.Name })) {
                $aliases = if (@($h.Aliases).Count -gt 0) { " (" + (@($h.Aliases) -join ', ') + ")" } else { '' }
                $known = if (Test-WtwSshHostKnown -Name $h.Name) { 'ssh ok' } else { 'not in ssh config — run: wtw host sync' }
                $knownColor = if ($known -eq 'ssh ok') { 'DarkGray' } else { 'Yellow' }
                Write-Host " $($h.Name)$aliases" -ForegroundColor Cyan -NoNewline
                Write-Host " $($h.User)@$($h.HostName) [$($h.Platform)]" -ForegroundColor White -NoNewline
                Write-Host " $known" -ForegroundColor $knownColor
                if (@($h.HostNames).Count -gt 1) {
                    Write-Host " candidates: $((@($h.HostNames)) -join ', ')" -ForegroundColor DarkGray
                }
            }
            Write-Host ''
        }

        'add' {
            if (-not $Name) { Write-Error 'Usage: wtw host add <name> [--alias a,b] [--user u] [--address ip] [--identity path] [--port n] [--platform windows|linux|macos]'; return }

            $config = Get-WtwConfig
            if (-not $config) { $config = New-WtwDefaultConfig }

            $hosts = Get-WtwPropertyValue -Object $config -Name 'hosts'
            if (-not $hosts) { $hosts = [PSCustomObject]@{} }

            $existing = Get-WtwPropertyValue -Object $hosts -Name $Name
            $entry = if ($existing) { $existing } else { [PSCustomObject]@{} }

            # Only overwrite what was actually passed, so `wtw host add x --platform windows`
            # is a targeted edit rather than a silent reset of the other fields.
            if ($Alias)    { $entry | Add-Member -NotePropertyName 'aliases' -NotePropertyValue (Split-WtwAliasList -Value $Alias) -Force }
            if ($User)     { $entry | Add-Member -NotePropertyName 'user' -NotePropertyValue $User -Force }
            if ($Address) {
                # Stored as an ordered candidate list. Put the mDNS name first
                # and a last-known IP second: on DHCP the address moves, the
                # .local name does not.
                $entry | Add-Member -NotePropertyName 'hostNames' -NotePropertyValue (Split-WtwAliasList -Value $Address) -Force
                # Drop the pre-0.2 single-value key so the two cannot disagree.
                if ((Get-WtwPropertyNames -Object $entry) -contains 'hostName') {
                    $entry.PSObject.Properties.Remove('hostName')
                }
            }
            if ($Identity) { $entry | Add-Member -NotePropertyName 'identityFile' -NotePropertyValue $Identity -Force; $entry | Add-Member -NotePropertyName 'identitiesOnly' -NotePropertyValue $true -Force }
            if ($Port)     { $entry | Add-Member -NotePropertyName 'port' -NotePropertyValue $Port -Force }
            if ($Wtw)      { $entry | Add-Member -NotePropertyName 'wtw' -NotePropertyValue $Wtw -Force }
            if ($Pwsh)     { $entry | Add-Member -NotePropertyName 'pwsh' -NotePropertyValue $Pwsh -Force }
            if ($PSBoundParameters.ContainsKey('Platform') -or -not (Get-WtwPropertyValue -Object $entry -Name 'platform')) {
                $entry | Add-Member -NotePropertyName 'platform' -NotePropertyValue $Platform -Force
            }

            $hosts | Add-Member -NotePropertyName $Name -NotePropertyValue $entry -Force
            $config | Add-Member -NotePropertyName 'hosts' -NotePropertyValue $hosts -Force
            Save-WtwConfig $config

            Write-Host " Saved host '$Name'." -ForegroundColor Green
            Sync-WtwSshConfig | Out-Null
        }

        'remove' {
            if (-not $Name) { Write-Error 'Usage: wtw host remove <name>'; return }
            $config = Get-WtwConfig
            $hosts = Get-WtwPropertyValue -Object $config -Name 'hosts'
            if (-not $hosts -or -not ((Get-WtwPropertyNames -Object $hosts) -contains $Name)) {
                Write-Error "Host '$Name' is not configured."
                return
            }
            $hosts.PSObject.Properties.Remove($Name)
            Save-WtwConfig $config
            Write-Host " Removed host '$Name'." -ForegroundColor Green
            Sync-WtwSshConfig | Out-Null
        }

        'sync' {
            $hosts = Get-WtwHosts
            if ($hosts.Count -eq 0) {
                Write-Host ' No hosts configured — nothing to sync.' -ForegroundColor Yellow
                return
            }
            Sync-WtwSshConfig | Out-Null
        }

        'trust' {
            if (-not $Name) { Write-Error 'Usage: wtw host trust <name>'; return }
            $entry = Resolve-WtwHost -Name $Name
            if (-not $entry) { Write-Error "Host '$Name' is not configured. See: wtw host list"; return }
            if (-not (Get-Command ssh-keyscan -ErrorAction SilentlyContinue)) {
                Write-Error "ssh-keyscan is not on PATH. Accept the key manually instead: ssh $($entry.Name)"
                return
            }

            # Every candidate address needs its own known_hosts entry — ssh
            # trusts a key per host pattern, so reaching the same machine by
            # .local after trusting its IP is a fresh, unverified connection.
            $targets = @($entry.Name) + @($entry.HostNames) | Where-Object { $_ } | Select-Object -Unique
            $port = if ($entry.Port) { [int]$entry.Port } else { 22 }

            $keys = @()
            foreach ($target in $targets) {
                $scanned = @(& ssh-keyscan -p $port -T 5 $target 2>$null | Where-Object { $_ -and $_ -notmatch '^#' })
                if ($scanned.Count -gt 0) { $keys += $scanned }
            }
            if ($keys.Count -eq 0) {
                Write-Error "ssh-keyscan got no keys for $($targets -join ', '). Is the machine up?"
                return
            }

            # Key material already in known_hosts, keyed by the base64 blob. A
            # scanned key that matches one you already trust under a different
            # name is the same machine answering to a new address — which turns
            # this from blind trust-on-first-use into a comparison you can check.
            $knownHostsPath = Join-Path $script:WtwSshConfigDir 'known_hosts'
            $trustedMaterial = @{}
            if (Test-Path $knownHostsPath) {
                foreach ($line in (Get-Content $knownHostsPath)) {
                    $parts = $line -split '\s+'
                    if ($parts.Count -ge 3 -and $parts[2]) { $trustedMaterial[$parts[2]] = $parts[0] }
                }
            }

            Write-Host ''
            Write-Host ' Host keys offered:' -ForegroundColor Cyan
            $anyAlreadyTrusted = $false
            foreach ($key in ($keys | Select-Object -Unique)) {
                $fields = $key -split '\s+'
                $fingerprint = '(fingerprint unavailable)'
                try {
                    $tmp = [System.IO.Path]::GetTempFileName()
                    Set-Content -Path $tmp -Value $key -Encoding ascii
                    $fingerprint = ((& ssh-keygen -l -f $tmp 2>$null) | Select-Object -First 1)
                    Remove-Item $tmp -Force -ErrorAction SilentlyContinue
                } catch { }

                $material = if ($fields.Count -ge 3) { $fields[2] } else { $null }
                $match = if ($material -and $trustedMaterial.ContainsKey($material)) { $trustedMaterial[$material] } else { $null }

                Write-Host " $($fields[0]) $fingerprint" -ForegroundColor White -NoNewline
                if ($match) {
                    $anyAlreadyTrusted = $true
                    Write-Host " ← same key you already trust for $match" -ForegroundColor Green
                } else {
                    Write-Host ''
                }
            }

            Write-Host ''
            if ($anyAlreadyTrusted) {
                Write-Host ' Keys marked above already match a host you trust, so those are the same' -ForegroundColor Green
                Write-Host ' machine reachable under another name.' -ForegroundColor Green
            } else {
                Write-Host ' None of these match a host you already trust. Verify against the machine:' -ForegroundColor Yellow
                Write-Host ' on that host, run: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub' -ForegroundColor DarkGray
                Write-Host ' (Windows OpenSSH: ssh-keygen -lf $env:ProgramData\ssh\ssh_host_ed25519_key.pub)' -ForegroundColor DarkGray
            }
            Write-Host ''

            $answer = Read-Host " Add these to ~/.ssh/known_hosts? [y/N]"
            if ($answer -notin @('y', 'Y', 'yes')) {
                Write-Host ' Not added.' -ForegroundColor DarkGray
                return
            }

            $knownHosts = Join-Path $script:WtwSshConfigDir 'known_hosts'
            if (-not (Test-Path $script:WtwSshConfigDir)) {
                New-Item -ItemType Directory -Path $script:WtwSshConfigDir -Force | Out-Null
            }
            $existing = if (Test-Path $knownHosts) { @(Get-Content $knownHosts) } else { @() }
            $added = 0
            foreach ($key in ($keys | Select-Object -Unique)) {
                if ($existing -notcontains $key) {
                    Add-Content -Path $knownHosts -Value $key -Encoding ascii
                    $added++
                }
            }
            Write-Host " Added $added key(s) to $knownHosts." -ForegroundColor Green
        }

        'test' {
            if (-not $Name) { Write-Error 'Usage: wtw host test <name>'; return }
            $entry = Resolve-WtwHost -Name $Name
            if (-not $entry) { Write-Error "Host '$Name' is not configured. See: wtw host list"; return }

            Write-Host ''
            Write-Host " addresses " -NoNewline
            $resolved = Resolve-WtwHostAddress -HostEntry $entry
            if ($resolved.Reachable) {
                Write-Host "$($resolved.Address) answers on ssh" -ForegroundColor Green
            } else {
                Write-Host "none of [$((@($entry.HostNames)) -join ', ')] answered" -ForegroundColor Red
            }

            Write-Host " ssh config " -NoNewline
            if (Test-WtwSshHostKnown -Name $entry.Name) {
                $configured = (& ssh -G $entry.Name 2>$null |
                        Where-Object { $_ -match '^hostname\s+(.+)$' } |
                        Select-Object -First 1) -replace '^hostname\s+', ''
                if ($resolved.Reachable -and $configured.Trim() -ne $resolved.Address) {
                    # The stored address went stale — normal on DHCP.
                    Write-Host "resolves to $($configured.Trim()), but $($resolved.Address) is the live one — run: wtw host sync" -ForegroundColor Yellow
                } else {
                    Write-Host 'resolves' -ForegroundColor Green
                }
            } else {
                Write-Host 'NOT resolvable — run: wtw host sync' -ForegroundColor Red
            }

            Write-Host " remote wtw " -NoNewline
            $probe = Invoke-WtwRemoteCommand -HostEntry $entry -Arguments @('__aliases')
            if ($probe.Success) {
                $count = @($probe.Output | Where-Object { $_ }).Count
                Write-Host "reachable ($count registered targets)" -ForegroundColor Green
            } else {
                Write-Host "unreachable" -ForegroundColor Red
                if ($probe.Error) {
                    foreach ($line in (Format-WtwSshError -HostEntry $entry -ErrorText $probe.Error) -split "`n") {
                        Write-Host " $line" -ForegroundColor DarkGray
                    }
                }
            }
            Write-Host ''
        }

        default {
            Write-Error "Unknown host action '$Action'. Use: list, add, remove, sync, trust, test."
        }
    }
}