public/Edit-WtwEntry.ps1
|
function Edit-WtwEntry { <# .SYNOPSIS Edit a registered repo or worktree (name, task key, aliases). .DESCRIPTION Updates the wtw registry record in place. Does not move git worktrees, rename branches, or delete files. Worktree: --name / second positional display (pretty) name --task registry key used by ``wtw go`` and derived aliases --alias extra typed names (e.g. ``onboarding video``) Repo: --alias / --name / second positional replace the alias list --key registry key (also remaps color keys) --emoji SourceGit / list prefix for the whole repo --sourcegit-folder / --no-sourcegit-folder create or skip a SourceGit group folder With no edit flags, prints the current record (cwd when name is omitted). .PARAMETER Name Repo alias, worktree, or alias-task (same resolution as go/info). .PARAMETER NewName Shorthand for --name: pretty name (worktree) or alias list (repo). .PARAMETER PrettyName Display name. A color-circle emoji is prepended for worktrees. .PARAMETER Task New worktree registry key. Does not rename the folder or branch. .PARAMETER Alias Comma-separated aliases (replaces the existing list). On a repo this is the typed names for ``wtw go``. On a worktree these are extra names (spaces allowed) in addition to the derived ``alias-task`` forms. Pass ``-`` / ``none`` to clear worktree aliases. .PARAMETER Key New repo registry key. .PARAMETER Emoji Repo-only display prefix (e.g. ``🎸``, ``🎭 ☸️``). ``-`` / ``none`` clears it. .PARAMETER Repo Disambiguate when the same task exists in multiple repos. .PARAMETER NoSync Skip rewriting the workspace file and SourceGit bookmark. .EXAMPLE wtw edit auth --name "Login flow" Change the worktree display name shown in list/info and editor sidebars. .EXAMPLE wtw rename auth login Same as ``wtw edit auth --name login``. .EXAMPLE wtw edit auth --task login Retarget ``wtw go login`` / ``sn-login`` without moving the checkout. .EXAMPLE wtw edit snowmain1 --alias sn,sm Replace the repo's typed aliases. .EXAMPLE wtw edit snowmain1 --emoji 🎸 Prefix SourceGit and ``wtw list`` with a repo emoji. .EXAMPLE wtw edit t3code-ad4f13f1 --alias 'onboarding video,onboarding' After adopting an agent worktree, ``wtw go onboarding`` resolves to it. #> [CmdletBinding()] param( [Parameter(Position = 0)] [string] $Name, [Parameter(Position = 1)] [string] $NewName, [string[]] $PrettyName, [string] $Task, [string[]] $Alias, [string] $Key, [AllowEmptyString()] [object] $Emoji, [switch] $SourceGitFolder, [switch] $NoSourceGitFolder, [string] $Repo, [Alias('ns')] [switch] $NoSync ) if (-not $PrettyName -and $NewName) { $PrettyName = @($NewName) } $nameFromCwd = -not $Name if (-not $Name) { $Name = Resolve-WtwCurrentTarget if (-not $Name) { Write-Error "Not inside a registered repo. Specify a target or cd into a repo." return } if ($nameFromCwd) { Write-Host " Detected: $Name" -ForegroundColor DarkGray } } $target = if ($Repo) { Resolve-WtwTarget -Name $Name -RepoAlias $Repo } else { Resolve-WtwTarget $Name } if (-not $target) { return } $prettyText = Get-WtwJoinedEditValue -Value $PrettyName $hasPretty = -not [string]::IsNullOrWhiteSpace($prettyText) $hasTask = -not [string]::IsNullOrWhiteSpace($Task) $hasAlias = @($Alias | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -gt 0 $hasKey = -not [string]::IsNullOrWhiteSpace($Key) $emojiBound = $PSBoundParameters.ContainsKey('Emoji') $folderBound = $SourceGitFolder -or $NoSourceGitFolder if ($emojiBound -and -not (Test-WtwEmojiArgument $Emoji)) { return } $hasEdits = $hasPretty -or $hasTask -or $hasAlias -or $hasKey -or $emojiBound -or $folderBound if (-not $hasEdits) { Show-WtwEditableRecord -Target $target return } if ($target.TaskName) { if ($hasKey) { Write-Error "--key is for repos. To rename this worktree's registry key, use --task." return } if ($emojiBound) { Write-Error "--emoji is for repos. Worktree names already get a color-circle prefix." return } if ($folderBound) { Write-Error "--sourcegit-folder is for repos. New worktrees follow the parent repo's folder." return } Edit-WtwWorktreeRecord ` -Target $target ` -PrettyName $prettyText ` -Task $Task ` -Alias $Alias ` -NoSync:$NoSync return } if ($hasTask) { Write-Error "--task is for worktrees. To rename this repo's registry key, use --key." return } $aliasInput = if ($hasAlias) { $Alias } elseif ($hasPretty) { $PrettyName } else { $null } Edit-WtwRepoRecord ` -Target $target ` -Alias $aliasInput ` -Key $Key ` -Emoji $Emoji ` -EmojiSpecified:$emojiBound ` -SourceGitFolder:$SourceGitFolder ` -NoSourceGitFolder:$NoSourceGitFolder ` -NoSync:$NoSync } function Get-WtwJoinedEditValue { param([AllowNull()] [string[]] $Value) if (-not $Value) { return $null } $joined = (@($Value | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ' ') if ([string]::IsNullOrWhiteSpace($joined)) { return $null } return $joined } function Show-WtwEditableRecord { param([Parameter(Mandatory)] [PSObject] $Target) $repoName = $Target.RepoName $repoEntry = $Target.RepoEntry $aliases = Get-WtwRepoAliases $repoEntry Write-Host '' if ($Target.TaskName) { $wt = $Target.WorktreeEntry $pretty = Get-WtwPropertyValue -Object $wt -Name 'prettyName' $color = Get-WtwPropertyValue -Object $wt -Name 'color' $ws = Get-WtwPropertyValue -Object $wt -Name 'workspace' $derived = ($aliases | ForEach-Object { "$_-$($Target.TaskName)" }) -join ', ' $custom = @(Get-WtwWorktreeAliases $wt) $shownAliases = @($custom + @($derived | Where-Object { $_ })) -join ', ' Write-Host " Worktree $repoName / $($Target.TaskName)" -ForegroundColor Cyan if ($pretty) { Write-Host " Name : $pretty" } Write-Host " Task : $($Target.TaskName)" Write-Host " Branch : $(Get-WtwPropertyValue -Object $wt -Name 'branch')" Write-Host " Path : $(Get-WtwPropertyValue -Object $wt -Name 'path')" if ($ws) { Write-Host " Workspace : $ws" } if ($color) { Write-Host " Color : $color" } if ($shownAliases) { Write-Host " Aliases : $shownAliases" } Write-Host '' Write-Host " wtw edit $($Target.TaskName) --name <pretty> display name" -ForegroundColor DarkGray Write-Host " wtw edit $($Target.TaskName) --task <key> go-target / derived aliases" -ForegroundColor DarkGray Write-Host " wtw edit $($Target.TaskName) --alias 'onboarding video' extra typed name" -ForegroundColor DarkGray } else { $emoji = Get-WtwRepoEmoji -RepoEntry $repoEntry $display = Format-WtwRepoDisplayName -Name $repoName -RepoEntry $repoEntry Write-Host " Repo $display" -ForegroundColor Cyan Write-Host " Key : $repoName" Write-Host " Emoji : $(if ($emoji) { $emoji } else { '(none)' })" $folderOn = $false if ((Get-WtwPropertyNames -Object $repoEntry) -contains 'sourceGitFolder') { $folderOn = [bool]$repoEntry.sourceGitFolder } Write-Host " Aliases : $($aliases -join ', ')" Write-Host " Path : $(Get-WtwPropertyValue -Object $repoEntry -Name 'mainPath')" Write-Host " SG folder : $(if ($folderOn) { 'yes' } else { 'if one already exists' })" Write-Host '' Write-Host " wtw edit $repoName --alias a,b typed names for wtw go" -ForegroundColor DarkGray Write-Host " wtw edit $repoName --key <name> registry key" -ForegroundColor DarkGray Write-Host " wtw edit $repoName --emoji 🎸 SourceGit / list prefix" -ForegroundColor DarkGray Write-Host " wtw edit $repoName --emoji none clear prefix" -ForegroundColor DarkGray Write-Host " wtw edit $repoName --sourcegit-folder group worktrees in SourceGit" -ForegroundColor DarkGray } Write-Host '' } function Edit-WtwWorktreeRecord { [CmdletBinding()] param( [Parameter(Mandatory)] [PSObject] $Target, [string] $PrettyName, [string] $Task, [string[]] $Alias, [switch] $NoSync ) $repoName = $Target.RepoName $oldTask = $Target.TaskName $newTask = $oldTask if (-not [string]::IsNullOrWhiteSpace($Task)) { $newTask = ConvertTo-WtwBranchSafeName -Name $Task if ([string]::IsNullOrWhiteSpace($newTask)) { Write-Error "Invalid --task '$Task'." return } if ($newTask -ne $Task) { Write-Host " Normalized task: $newTask" -ForegroundColor DarkCyan } } $registry = Get-WtwRegistry $worktrees = $registry.repos.$repoName.worktrees if (-not $worktrees -or ((Get-WtwPropertyNames -Object $worktrees) -notcontains $oldTask)) { Write-Error "Worktree '$oldTask' is not in the registry." return } if ($newTask -ne $oldTask -and ((Get-WtwPropertyNames -Object $worktrees) -contains $newTask)) { Write-Error "Worktree '$newTask' is already registered under '$repoName'." return } $entry = $worktrees.$oldTask $color = Get-WtwPropertyValue -Object $entry -Name 'color' $changed = @() if (-not [string]::IsNullOrWhiteSpace($PrettyName)) { $newPretty = if ($color) { Format-WtwPrettyNameWithCircle -Hex $color -Name $PrettyName } else { $PrettyName.Trim() } $oldPretty = Get-WtwPropertyValue -Object $entry -Name 'prettyName' if ($newPretty -ne $oldPretty) { $entry | Add-Member -NotePropertyName 'prettyName' -NotePropertyValue $newPretty -Force $changed += "name '$oldPretty' → '$newPretty'" } } if (@($Alias | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) { if (Test-WtwAliasClearToken -Value $Alias) { $oldAliases = Get-WtwWorktreeAliases $entry if ((Get-WtwPropertyNames -Object $entry) -contains 'aliases') { $entry.PSObject.Properties.Remove('aliases') } if ((Get-WtwPropertyNames -Object $entry) -contains 'alias') { $entry.PSObject.Properties.Remove('alias') } if (@($oldAliases).Count -gt 0) { $changed += "aliases '$($oldAliases -join ',')' → '(none)'" } } else { [string[]] $aliasArray = Split-WtwAliasList -Value $Alias if ($aliasArray.Count -eq 0) { Write-Error "Invalid --alias '$($Alias -join ',')'." return } $collision = Test-WtwWorktreeAliasCollision -Registry $registry -RepoName $repoName -TaskName $newTask -Aliases $aliasArray if ($collision) { return } $oldAliases = Get-WtwWorktreeAliases $entry $entry | Add-Member -NotePropertyName 'aliases' -NotePropertyValue $aliasArray -Force if ((Get-WtwPropertyNames -Object $entry) -contains 'alias') { $entry.PSObject.Properties.Remove('alias') } $changed += "aliases '$($oldAliases -join ',')' → '$($aliasArray -join ',')'" } } if ($newTask -ne $oldTask) { $worktrees = Rename-WtwObjectProperty -Object $worktrees -OldName $oldTask -NewName $newTask $registry.repos.$repoName.worktrees = $worktrees Rename-WtwColorAssignmentKey -OldKey "$repoName/$oldTask" -NewKey "$repoName/$newTask" $changed += "task '$oldTask' → '$newTask'" } else { $registry.repos.$repoName.worktrees.$oldTask = $entry } if ($changed.Count -eq 0) { Write-Host ' Nothing to change.' -ForegroundColor DarkGray return } Save-WtwRegistry $registry $saved = $registry.repos.$repoName.worktrees.$newTask $prettyNow = Get-WtwPropertyValue -Object $saved -Name 'prettyName' $wsFile = Get-WtwPropertyValue -Object $saved -Name 'workspace' $wtPath = Get-WtwPropertyValue -Object $saved -Name 'path' if (-not $NoSync) { if ($wsFile -and (Test-Path $wsFile)) { Update-WtwWorkspaceIdentity ` -Path $wsFile ` -PrettyName $prettyNow ` -TaskName $newTask if ($prettyNow) { $newWsPath = Get-WtwCursorPrettyWorkspacePath -WorkspacePath $wsFile -PrettyName $prettyNow -RepoName $repoName $newWsPath = [System.IO.Path]::GetFullPath($newWsPath) $oldWsPath = [System.IO.Path]::GetFullPath($wsFile) if ($newWsPath -ne $oldWsPath) { $canMigrate = Resolve-WtwCursorStateConflict -PrettyName $prettyNow if ($canMigrate) { $migratedWorkspace = Move-WtwCursorWorkspaceForAgents ` -WorkspacePath $wsFile ` -PrettyName $prettyNow ` -RepoName $repoName if ($migratedWorkspace -and $migratedWorkspace -ne $wsFile) { $saved | Add-Member -NotePropertyName 'workspace' -NotePropertyValue $migratedWorkspace -Force $registry.repos.$repoName.worktrees.$newTask = $saved Save-WtwRegistry $registry $wsFile = $migratedWorkspace Write-Host " Workspace file: $(Split-Path $wsFile -Leaf)" -ForegroundColor DarkGray } } else { Write-Host ' Keeping existing workspace filename (Cursor Agents label not migrated).' -ForegroundColor DarkGray } } } Write-Host ' Syncing workspace...' -ForegroundColor DarkGray Sync-WtwWorkspace -Target $wsFile -ColorSource Json } if ($wtPath -and $prettyNow) { Add-WtwSourceGitRepository -Path $wtPath -Name $prettyNow -Hex $color -RepoName $repoName } } Write-Host '' Write-Host " Updated $repoName / $newTask ($($changed -join '; '))." -ForegroundColor Green Write-Host '' } function Edit-WtwRepoRecord { [CmdletBinding()] param( [Parameter(Mandatory)] [PSObject] $Target, [string[]] $Alias, [string] $Key, [AllowEmptyString()] [object] $Emoji, [switch] $EmojiSpecified, [switch] $SourceGitFolder, [switch] $NoSourceGitFolder, [switch] $NoSync ) $oldKey = $Target.RepoName $newKey = $oldKey $workspacesToUpdate = @() if (-not [string]::IsNullOrWhiteSpace($Key)) { $newKey = $Key.Trim() if ([string]::IsNullOrWhiteSpace($newKey)) { Write-Error "Invalid --key '$Key'." return } } $registry = Get-WtwRegistry if ((Get-WtwPropertyNames -Object $registry.repos) -notcontains $oldKey) { Write-Error "Repo '$oldKey' is not in the registry." return } $entry = $registry.repos.$oldKey $changed = @() if (@($Alias | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) { # Split-WtwAliasList returns `,@(...)` so a naive `@()` wrap nests it. [string[]] $aliasArray = Split-WtwAliasList -Value $Alias if ($aliasArray.Count -eq 0) { Write-Error "Invalid --alias '$($Alias -join ',')'." return } $collision = Test-WtwRepoIdentityCollision -Registry $registry -RepoName $oldKey -NewKey $oldKey -Aliases $aliasArray if ($collision) { return } $oldAliases = Get-WtwRepoAliases $entry $entry | Add-Member -NotePropertyName 'aliases' -NotePropertyValue $aliasArray -Force if ((Get-WtwPropertyNames -Object $entry) -contains 'alias') { $entry.PSObject.Properties.Remove('alias') } $changed += "aliases '$($oldAliases -join ',')' → '$($aliasArray -join ',')'" } $previousDisplay = Format-WtwRepoDisplayName -Name $oldKey -RepoEntry $entry if ($EmojiSpecified) { $oldEmoji = Get-WtwRepoEmoji -RepoEntry $entry $newEmoji = Set-WtwRepoEmojiProperty -RepoEntry $entry -Emoji $Emoji if ($oldEmoji -ne $newEmoji) { $oldShown = if ($oldEmoji) { $oldEmoji } else { '(none)' } $newShown = if ($newEmoji) { $newEmoji } else { '(none)' } $changed += "emoji '$oldShown' → '$newShown'" } } if ($SourceGitFolder -and $NoSourceGitFolder) { Write-Error "Pass --sourcegit-folder or --no-sourcegit-folder, not both." return } if ($SourceGitFolder -or $NoSourceGitFolder) { $wantFolder = [bool]$SourceGitFolder $oldFolder = $false if ((Get-WtwPropertyNames -Object $entry) -contains 'sourceGitFolder') { $oldFolder = [bool]$entry.sourceGitFolder } $entry | Add-Member -NotePropertyName 'sourceGitFolder' -NotePropertyValue $wantFolder -Force if ($oldFolder -ne $wantFolder) { $changed += "sourceGitFolder '$oldFolder' → '$wantFolder'" } } if ($newKey -ne $oldKey) { if ((Get-WtwPropertyNames -Object $registry.repos) -contains $newKey) { Write-Error "Repo '$newKey' is already in the registry." return } $aliasesForCheck = if (@($Alias | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) { [string[]](Split-WtwAliasList -Value $Alias) } else { Get-WtwRepoAliases $entry } $collision = Test-WtwRepoIdentityCollision -Registry $registry -RepoName $oldKey -NewKey $newKey -Aliases $aliasesForCheck if ($collision) { return } $workspacesToUpdate = @() if ($entry.worktrees) { foreach ($task in (Get-WtwPropertyNames -Object $entry.worktrees)) { $ws = Get-WtwPropertyValue -Object $entry.worktrees.$task -Name 'workspace' if ($ws -and (Test-Path $ws)) { $workspacesToUpdate += $ws } } } if (-not $NoSync) { foreach ($ws in $workspacesToUpdate) { if (-not (Update-WtwWorkspaceIdentity -Path $ws -RepoName $newKey)) { Write-Error "Could not update workspace '$ws' with repo key '$newKey'. Registry was not saved." return } } } $registry.repos.$oldKey = $entry $registry.repos = Rename-WtwObjectProperty -Object $registry.repos -OldName $oldKey -NewName $newKey Rename-WtwColorAssignmentRepoPrefix -OldRepo $oldKey -NewRepo $newKey $changed += "key '$oldKey' → '$newKey'" } else { $registry.repos.$oldKey = $entry } if ($changed.Count -eq 0) { Write-Host ' Nothing to change.' -ForegroundColor DarkGray return } Save-WtwRegistry $registry if ($SourceGitFolder -and -not $NoSync) { $savedForFolder = (Get-WtwRegistry).repos.$newKey if ($savedForFolder) { Ensure-WtwSourceGitRepoFolder -RepoName $newKey -RepoEntry $savedForFolder -Force } } if ($newKey -ne $oldKey -and -not $NoSync) { foreach ($ws in @($workspacesToUpdate)) { Write-Host ' Syncing workspace...' -ForegroundColor DarkGray Sync-WtwWorkspace -Target $ws -ColorSource Json } } if (-not $NoSync -and ($EmojiSpecified -or $newKey -ne $oldKey)) { $saved = (Get-WtwRegistry).repos.$newKey if ($saved) { Sync-WtwSourceGitRepoDisplayName ` -RepoName $newKey ` -RepoEntry $saved ` -PreviousDisplayName $previousDisplay } } Write-Host '' Write-Host " Updated repo $newKey ($($changed -join '; '))." -ForegroundColor Green Write-Host " New shell aliases apply in a new terminal, or re-source the wtw wrapper." -ForegroundColor DarkGray Write-Host '' } function Test-WtwRepoIdentityCollision { <# .SYNOPSIS Return $true (and write the error) when a proposed repo key/aliases collide. #> param( [Parameter(Mandatory)] [PSObject] $Registry, [Parameter(Mandatory)] [string] $RepoName, [Parameter(Mandatory)] [string] $NewKey, [AllowEmptyCollection()] [string[]] $Aliases ) foreach ($existingName in (Get-WtwPropertyNames -Object $Registry.repos)) { if ($existingName -eq $RepoName) { continue } $existingRepo = $Registry.repos.$existingName $existingAliases = Get-WtwRepoAliases $existingRepo foreach ($newAlias in @($Aliases)) { if ($newAlias -eq $existingName -or $newAlias -in $existingAliases) { Write-Error "Alias '$newAlias' is already used by repo '$existingName'." return $true } } if ($NewKey -eq $existingName -or $NewKey -in $existingAliases) { Write-Error "Registry key '$NewKey' collides with repo '$existingName'." return $true } } return $false } function Update-WtwWorkspaceIdentity { <# .SYNOPSIS Write pretty-name / task metadata into an existing workspace file. .DESCRIPTION Runs before Sync-WtwWorkspace so a single-file sync (which reads identity from the file, not the registry) picks up the new values. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [string] $PrettyName, [string] $TaskName, [string] $RepoName ) $workspace = Read-JsoncFile $Path if (-not $workspace) { return $false } $settings = Get-WtwPropertyValue -Object $workspace -Name 'settings' if (-not $settings) { return $false } if (-not [string]::IsNullOrWhiteSpace($PrettyName)) { $settings | Add-Member -NotePropertyName 'wtw.prettyName' -NotePropertyValue $PrettyName -Force $windowTitle = '{0}${{separator}}${{dirty}}${{activeEditorShort}}${{separator}}${{appName}}' -f $PrettyName $settings | Add-Member -NotePropertyName 'window.title' -NotePropertyValue $windowTitle -Force } if (-not [string]::IsNullOrWhiteSpace($TaskName)) { $settings | Add-Member -NotePropertyName 'wtw.task' -NotePropertyValue $TaskName -Force } if (-not [string]::IsNullOrWhiteSpace($RepoName)) { $settings | Add-Member -NotePropertyName 'wtw.repo' -NotePropertyValue $RepoName -Force } try { $workspace | ConvertTo-Json -Depth 20 | Set-Content -Path $Path -Encoding utf8 return $true } catch { Write-Error "Failed to write workspace identity to '$Path': $_" return $false } } |