private/Get-WtwEditorFamily.ps1
|
function Get-WtwEditorFamily { <# .SYNOPSIS The VS Code editor family table — single source of truth. .DESCRIPTION Every VS Code fork wtw supports shares four capabilities: a CLI that takes a path, `--list-extensions`, a user `settings.json`, and support for `vscode-remote://` authorities. That shared shape is what makes them a family; SourceGit / T3 / ChatGPT / cmux / Superset share none of it and stay `type`-tagged special cases in Resolve-WtwEditorCommand. Before this table the same five editors were listed in four places — Resolve-WtwEditorCommand (prefixes), Invoke-WtwEditorCli (CLI candidates + app bundles), Install-Wtw (detection + mac hints), and the completion script. They drifted: the Antigravity v1→v2 CLI rename fixed launch-time invocation but not install-time detection. Remote-SSH would have added a fifth and sixth list (extension id, settings dir), so the lists are now projections of this one. Member fields: Id Canonical name — also the CLI name wtw resolves to. Name Display name used in install output. Prefixes CLI shortcuts, longest-lived first (matched by prefix). Cli Ordered CLI candidates, newest first. Survives renames. MacApps Ordered /Applications bundle names. Index-paired with Cli when the counts match, so Antigravity IDE.app maps to antigravity-ide and Antigravity.app to antigravity. LaunchFlags Flags always passed before the path. RemoteExts Ordered Remote-SSH extension ids. VSCodium cannot use Microsoft's (proprietary, absent from Open VSX), hence a per-member list rather than one shared constant. SettingsDirs Ordered config-dir candidates under the platform's user settings root. First existing wins. .OUTPUTS Ordered array of hashtables. Order is significant: Resolve-WtwEditorCommand matches prefixes in this order, so `wtw c...` keeps resolving to Cursor. #> [CmdletBinding()] param() return @( @{ Id = 'cursor' Name = 'Cursor' Prefixes = @('cursor', 'cur') Cli = @('cursor') MacApps = @('Cursor') # Cursor otherwise follows its last-window behaviour, which can route a # workspace into a standalone Agent window rather than a project IDE. # wtw worktrees need their own IDE window so the project context stays # unambiguous. LaunchFlags = @('--new-window') RemoteExts = @('anysphere.remote-ssh') SettingsDirs = @('Cursor') } @{ Id = 'code' Name = 'VS Code' Prefixes = @('code', 'co') Cli = @('code') MacApps = @('Visual Studio Code') LaunchFlags = @() RemoteExts = @('ms-vscode-remote.remote-ssh') SettingsDirs = @('Code') } @{ Id = 'antigravity' Name = 'Antigravity' Prefixes = @('antigravity', 'anti', 'ag') # v2 ships its CLI as `antigravity-ide` (app: "Antigravity IDE.app") # while the v1 installer's `~/.antigravity/.../bin/antigravity` stub # lingers and points at a binary that no longer exists. Cli = @('antigravity-ide', 'antigravity') MacApps = @('Antigravity IDE', 'Antigravity') LaunchFlags = @() RemoteExts = @('google.antigravity-remote-ssh', 'ms-vscode-remote.remote-ssh') SettingsDirs = @('Antigravity IDE', 'Antigravity') } @{ Id = 'windsurf' Name = 'Windsurf' Prefixes = @('windsurf', 'wind', 'ws') Cli = @('windsurf') MacApps = @('Windsurf') LaunchFlags = @() RemoteExts = @('codeium.windsurf-remote-openssh', 'jeanp413.open-remote-ssh') SettingsDirs = @('Windsurf') } @{ Id = 'codium' Name = 'VSCodium' Prefixes = @('codium', 'vscodium') Cli = @('codium') MacApps = @('VSCodium') LaunchFlags = @() # Microsoft's Remote-SSH is proprietary and not published to Open VSX, # so VSCodium needs the community fork instead. RemoteExts = @('jeanp413.open-remote-ssh') SettingsDirs = @('VSCodium') } ) } function Get-WtwEditorFamilyMember { <# .SYNOPSIS Look up one family member by its canonical id. .PARAMETER Id Canonical editor id (e.g. 'cursor'). Not a prefix — use Resolve-WtwEditorFamilyMember for shortcut resolution. .OUTPUTS The member hashtable, or $null when the id is not a family editor. #> [CmdletBinding()] param([AllowNull()][string] $Id) if (-not $Id) { return $null } return (Get-WtwEditorFamily | Where-Object { $_.Id -eq $Id } | Select-Object -First 1) } function Resolve-WtwEditorFamilyMember { <# .SYNOPSIS Resolve a user-typed shortcut to a family member. .DESCRIPTION Same prefix semantics as Resolve-WtwEditorCommand, but returns the whole member so callers needing RemoteExts / SettingsDirs don't have to resolve the id and then look it up again. .PARAMETER Name Editor name or shortcut ('cur', 'cursor', 'co', ...). #> [CmdletBinding()] param([AllowNull()][string] $Name) if (-not $Name) { return $null } foreach ($member in Get-WtwEditorFamily) { foreach ($prefix in $member.Prefixes) { if ($prefix -eq $Name -or $prefix.StartsWith($Name, [System.StringComparison]::OrdinalIgnoreCase)) { return $member } } } return $null } function Get-WtwEditorMacCliHints { <# .SYNOPSIS macOS "CLI is missing but the app is installed" probes for one member. .DESCRIPTION Derived from MacApps + Cli rather than a second hand-maintained table. When the two lists are the same length they are paired by index (so "Antigravity IDE.app" pairs with `antigravity-ide`); otherwise every combination is emitted. Either way the caller filters by Test-Path, so a wrong pairing simply never matches. .OUTPUTS Array of @{ App; Bin; Name } — the bundle, its bundled CLI, and the CLI name. #> [CmdletBinding()] param([Parameter(Mandatory)] $Member) $apps = @($Member.MacApps) $clis = @($Member.Cli) if ($apps.Count -eq 0 -or $clis.Count -eq 0) { return @() } $pairs = if ($apps.Count -eq $clis.Count) { 0..($apps.Count - 1) | ForEach-Object { @{ App = $apps[$_]; Cli = $clis[$_] } } } else { foreach ($app in $apps) { foreach ($cli in $clis) { @{ App = $app; Cli = $cli } } } } # Comma operator: a bare `return @(...)` unrolls a one-element array back to # the hashtable itself, and `$hints[0]` on a hashtable is a key lookup that # silently yields $null. Every family member with a single bundle hits this. return , @($pairs | ForEach-Object { @{ App = "/Applications/$($_.App).app" Bin = "/Applications/$($_.App).app/Contents/Resources/app/bin/$($_.Cli)" Name = $_.Cli } }) } function Get-WtwEditorSettingsPath { <# .SYNOPSIS Path to a family member's user settings.json on this platform. .DESCRIPTION VS Code forks keep user settings under a per-app directory in the platform's roaming-config root. Returns the first candidate whose User directory already exists — wtw never creates an editor's profile directory, so a not-installed editor yields $null instead of a path that would materialise a phantom profile. .PARAMETER Member Family member hashtable. #> [CmdletBinding()] param([Parameter(Mandatory)] $Member) $root = if ($IsWindows) { $env:APPDATA } elseif ($IsMacOS) { Join-Path $HOME 'Library' 'Application Support' } else { if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' } } if (-not $root) { return $null } foreach ($dir in @($Member.SettingsDirs)) { $userDir = Join-Path $root $dir 'User' if (Test-Path $userDir) { return (Join-Path $userDir 'settings.json') } } return $null } |