public/Sync-WtwWorkspace.ps1

function Resolve-WtwSyncTargetFromFile {
    <#
    .SYNOPSIS
        Build a sync target object from a workspace file path, resolving color source.
    #>

    param(
        [string] $TargetPath,
        [string] $ColorSource,
        [string] $TemplateOverride
    )

    $wsContent = Read-JsoncFile $TargetPath
    if (-not $wsContent) { return $null }

    $rn = $wsContent.settings.'wtw.repo'
    $tn = $wsContent.settings.'wtw.task'

    $colors = Get-WtwColors
    $taskKey = if ($tn -and $rn) {
        $reg = Get-WtwRegistry
        $repoEntry = if ($rn -and $reg.repos.PSObject.Properties.Name -contains $rn) { $reg.repos.$rn } else { $null }
        # wtw.task may store the workspace name (e.g. "repo_task") rather than the
        # registry worktree key ("task"). Try exact match first, then strip repo prefix.
        $wtName = $tn
        if ($repoEntry -and $repoEntry.worktrees -and -not ($repoEntry.worktrees.PSObject.Properties.Name -contains $wtName)) {
            $prefix = "${rn}_"
            if ($tn.StartsWith($prefix) -and $tn.Length -gt $prefix.Length) { $wtName = $tn.Substring($prefix.Length) }
        }
        if ($repoEntry -and $repoEntry.worktrees -and $repoEntry.worktrees.PSObject.Properties.Name -contains $wtName) { "$rn/$wtName" } else { "$rn/main" }
    } else { $null }
    $authColor = if ($taskKey -and $colors.assignments.PSObject.Properties.Name -contains $taskKey) { $colors.assignments.$taskKey } else { $null }
    $workspacePeacockColor = $wsContent.settings.'peacock.color'

    # Determine color source preference
    $canPrompt = [Environment]::UserInteractive
    try { $canPrompt = $canPrompt -and [bool]$Host.UI.RawUI } catch { $canPrompt = $false }

    $preferWorkspace = $false
    if ($ColorSource -eq 'Workspace') {
        $preferWorkspace = $true
    } elseif ($ColorSource -eq 'Json') {
        $preferWorkspace = $false
    } elseif ($canPrompt) {
        Write-Host ''
        Write-Host ' Which color should drive this sync?' -ForegroundColor Cyan
        Write-Host ' [J] colors.json assignment (default)' -ForegroundColor Gray
        Write-Host ' [W] peacock.color in the workspace file' -ForegroundColor Gray
        $reply = Read-Host ' Press J or W (Enter = J)'
        $preferWorkspace = (($reply ?? '').Trim()) -match '^[Ww]'
    }

    $resolvedColor = if ($preferWorkspace) { $workspacePeacockColor ?? $authColor } else { $authColor ?? $workspacePeacockColor }

    return [PSCustomObject]@{
        wsFile         = $TargetPath
        repoName       = $rn
        wsName         = $tn ?? [System.IO.Path]::GetFileNameWithoutExtension($TargetPath)
        codeFolderPath = $wsContent.settings.'wtw.worktreePath' ?? ($wsContent.folders[0].path)
        color          = $resolvedColor
        branch         = $wsContent.settings.'wtw.branch'
        worktreePath   = $wsContent.settings.'wtw.worktreePath'
        templatePath   = $(if ($TemplateOverride) { $TemplateOverride } else { $wsContent.settings.'wtw.templateSource' })
        isManaged      = [bool]$wsContent.settings.'wtw.managed'
    }
}

function Resolve-WtwWorkspaceFile {
    <#
    .SYNOPSIS
        Resolve a name/alias/path to a workspace file path.
    #>

    param(
        [string] $Target,
        [string] $WsDir
    )

    # Try as a file path first
    $path = if ([System.IO.Path]::IsPathRooted($Target)) { $Target } else { Join-Path $WsDir $Target }
    if (Test-Path $path) { return $path }

    # Resolve as repo/worktree name
    $resolved = Resolve-WtwTarget $Target
    if (-not $resolved) { return $null }
    $wsFile = if ($resolved.WorktreeEntry) { $resolved.WorktreeEntry.workspace } else { $resolved.RepoEntry.templateWorkspace }
    if ($wsFile -and (Test-Path $wsFile)) { return $wsFile }

    Write-Error "No workspace file found for '$Target'."
    return $null
}

function Sync-WtwWorkspace {
    <#
    .SYNOPSIS
        Re-apply template settings to managed workspace files.
    .DESCRIPTION
        Regenerates workspace files from their template while preserving colors.
        Can target a single workspace by name/path, or sync all managed workspaces
        with --All. Supports dry-run mode and template overrides.
    .PARAMETER Target
        Workspace name, alias, or file path to sync. Detects from cwd if omitted.
    .PARAMETER All
        Sync all managed workspaces across registered repos.
    .PARAMETER DryRun
        Preview what would be synced without writing any files.
    .PARAMETER Force
        Sync even if the workspace is not marked as wtw-managed.
    .PARAMETER Template
        Override the template source path for this sync operation.
    .PARAMETER Repo
        Limit --All scope to a specific repo alias or name.
    .PARAMETER ColorSource
        Choose color precedence: 'Json' (colors.json first) or 'Workspace' (peacock.color first). Omit to prompt interactively.
    .EXAMPLE
        wtw sync --all --dry-run
        Preview syncing all managed workspaces without making changes.
    .EXAMPLE
        wtw sync auth --color-source json
        Sync the "auth" workspace using colors.json as the color authority.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string] $Target,

        [switch] $All,
        [switch] $DryRun,
        [switch] $Force,
        [string] $Template,  # override template source for this sync
        [string] $Repo,      # limit --all to a specific repo

        # Single-file sync only: prefer colors.json (default) vs workspace peacock.color. Omit to prompt when interactive.
        [ValidateSet('Json', 'Workspace', IgnoreCase = $true)]
        [string] $ColorSource
    )

    $config = Get-WtwConfig
    if (-not $config) {
        Write-Error 'wtw not initialized. Run "wtw init" first.'
        return
    }

    $registry = Get-WtwRegistry
    $wsDir = $config.workspacesDir.Replace('~', $HOME)
    $wsDir = [System.IO.Path]::GetFullPath($wsDir)

    # Resolve template override
    $templateOverride = $null
    if ($Template) {
        $templateOverride = [System.IO.Path]::GetFullPath($Template)
        if (-not (Test-Path $templateOverride)) {
            Write-Error "Template not found: $templateOverride"
            return
        }
    }

    # Collect sync targets
    $syncTargets = @()

    if ($Target) {
        $targetPath = Resolve-WtwWorkspaceFile $Target $wsDir
        if (-not $targetPath) { return }
        $item = Resolve-WtwSyncTargetFromFile $targetPath $ColorSource $templateOverride
        if ($item) { $syncTargets += $item }

    } elseif ($All) {
        # All registered repos — main workspace + worktree workspaces
        foreach ($repoName in $registry.repos.PSObject.Properties.Name) {
            $repoEntry = $registry.repos.$repoName
            if ($Repo -and -not (Test-WtwAliasMatch $repoEntry $Repo) -and $repoName -ne $Repo) { continue }

            $tpl = $templateOverride ?? $repoEntry.template ?? $repoEntry.templateWorkspace
            $repoDir = Split-Path $repoEntry.mainPath -Leaf

            # Main workspace
            if ($repoEntry.templateWorkspace -and (Test-Path $repoEntry.templateWorkspace)) {
                $colors = Get-WtwColors
                $mainColor = $colors.assignments."$repoName/main"
                $syncTargets += [PSCustomObject]@{
                    wsFile         = $repoEntry.templateWorkspace
                    repoName       = $repoName
                    wsName         = $repoDir
                    codeFolderPath = $repoEntry.mainPath
                    color          = $mainColor
                    branch         = $null
                    worktreePath   = $null
                    templatePath   = $tpl
                    isManaged      = $true
                }
            }

            # Worktree workspaces
            if ($repoEntry.worktrees) {
                foreach ($taskName in $repoEntry.worktrees.PSObject.Properties.Name) {
                    $wt = $repoEntry.worktrees.$taskName
                    if ($wt.workspace -and (Test-Path $wt.workspace)) {
                        $syncTargets += [PSCustomObject]@{
                            wsFile         = $wt.workspace
                            repoName       = $repoName
                            wsName         = "${repoName}_${taskName}"
                            codeFolderPath = $wt.path
                            color          = $wt.color
                            branch         = $wt.branch
                            worktreePath   = $wt.path
                            templatePath   = $tpl
                            isManaged      = $true
                        }
                    }
                }
            }
        }
    } else {
        # No target, no --all: detect from cwd
        $detected = Resolve-WtwCurrentTarget
        if (-not $detected) {
            Write-Host ''
            Write-Host ' Usage:' -ForegroundColor Yellow
            Write-Host ' wtw sync [name] [--dry-run] Sync current or named workspace'
            Write-Host ' wtw sync --all [--dry-run] Sync all managed workspaces'
            Write-Host ' wtw sync --all --repo sn3 Sync one repo only'
            Write-Host ' wtw sync --all --template <path> Sync all with a new template'
            Write-Host ' wtw sync <name> --color-source json Skip prompt; use colors.json first'
            Write-Host ' wtw sync <name> --color-source workspace Skip prompt; use workspace peacock first'
            Write-Host ''
            return
        }
        Write-Host " Detected: $detected" -ForegroundColor DarkGray
        $targetPath = Resolve-WtwWorkspaceFile $detected $wsDir
        if (-not $targetPath) { return }
        $item = Resolve-WtwSyncTargetFromFile $targetPath $ColorSource $templateOverride
        if ($item) { $syncTargets += $item }
    }

    if ($syncTargets.Count -eq 0) {
        Write-Host ' No managed workspaces to sync.' -ForegroundColor DarkGray
        return
    }

    Write-Host ''
    Write-Host " Syncing $($syncTargets.Count) workspace(s)..." -ForegroundColor Cyan
    $synced = 0

    foreach ($item in $syncTargets) {
        if (-not $item.isManaged -and -not $Force) {
            Write-Host " SKIP: $($item.wsFile) (not wtw-managed, use --force)" -ForegroundColor Yellow
            continue
        }

        $tpl = $item.templatePath
        if (-not $tpl -or -not (Test-Path $tpl)) {
            Write-Host " SKIP: $(Split-Path $item.wsFile -Leaf) (template not found)" -ForegroundColor Yellow
            continue
        }

        if (-not $item.codeFolderPath) {
            Write-Host " SKIP: $(Split-Path $item.wsFile -Leaf) (cannot determine code folder)" -ForegroundColor Yellow
            continue
        }

        if ($DryRun) {
            Write-Host " WOULD SYNC: $(Split-Path $item.wsFile -Leaf) (template: $(Split-Path $tpl -Leaf))" -ForegroundColor DarkGray
            continue
        }

        New-WtwWorkspaceFile `
            -RepoName ($item.repoName ?? 'unknown') `
            -Name $item.wsName `
            -CodeFolderPath $item.codeFolderPath `
            -TemplatePath $tpl `
            -OutputPath $item.wsFile `
            -Color $item.color `
            -Branch $item.branch `
            -WorktreePath $item.worktreePath `
            -Managed | Out-Null

        $synced++
        Write-Host " SYNCED: $(Split-Path $item.wsFile -Leaf)" -ForegroundColor Green
    }

    Write-Host ''
    if ($DryRun) {
        Write-Host " (dry-run: $($syncTargets.Count) workspace(s) would be synced)" -ForegroundColor DarkGray
    } else {
        Write-Host " Synced $synced workspace(s)." -ForegroundColor Green
    }
    Write-Host ''
}