src/PowerShell/Workspace.ps1

# cdp PowerShell domain: Workspace.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function Get-CdpWorkspaceLauncher {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Open
    )

    $launcherName = $Open.Trim()
    if ($launcherName -notmatch '^[A-Za-z0-9._:/\\-]+$') {
        throw 'Launcher must be a single executable name or safe path without arguments.'
    }
    $normalizedName = $launcherName.ToLowerInvariant()
    $command = $launcherName
    $arguments = @()
    $label = $launcherName

    switch ($normalizedName) {
        { $_ -in @('code', 'vscode') } {
            $command = 'code'
            $arguments = @('.')
            $label = 'VS Code'
        }
        'cursor' {
            $command = 'cursor'
            $arguments = @('.')
            $label = 'Cursor'
        }
        'codex' { $label = 'Codex' }
        'claude' { $label = 'Claude' }
        'gemini' { $label = 'Gemini' }
    }

    [PSCustomObject]@{
        Name = $launcherName
        Label = $label
        Command = $command
        Arguments = $arguments
    }
}

function Invoke-CdpWorkspaceLauncher {
    param(
        [Parameter(Mandatory = $true)]
        [object]$Project,

        [Parameter(Mandatory = $true)]
        [string]$Open,

        [Parameter(Mandatory = $false)]
        [switch]$WSL
    )

    if ([string]::IsNullOrWhiteSpace($Open)) {
        return
    }

    $launcher = Get-CdpWorkspaceLauncher -Open $Open
    $resolution = Resolve-CdpProjectPath -Project $Project -Profile $(if ($WSL) { 'wsl' } else { $null })
    if ($resolution.ErrorCode) { throw $resolution.ErrorMessage }
    $workingDirectory = $resolution.ResolvedPath

    $result = [PSCustomObject]@{
        ProjectName = [string]$Project.name
        WorkingDirectory = $workingDirectory
        WSL = [bool]$WSL
        Name = $launcher.Name
        Label = $launcher.Label
        Command = $launcher.Command
        Arguments = @($launcher.Arguments)
    }

    if ($env:CDP_OPEN_DRY_RUN -eq '1') {
        Write-Host "Would open $($Project.name) with $($launcher.Label)." -ForegroundColor Gray
        return $result
    }

    Write-Host "Opening with $($launcher.Label)..." -ForegroundColor Cyan
    if ($WSL) {
        wsl --cd $workingDirectory --exec $launcher.Command @($launcher.Arguments)
        return
    }

    if (-not (Get-Command $launcher.Command -ErrorAction SilentlyContinue)) {
        Write-Host "Error: '$($launcher.Command)' command not found." -ForegroundColor Red
        return
    }

    & $launcher.Command @($launcher.Arguments)
}

function Get-CdpWorkspacesPath {
    param([string]$ConfigPath)

    if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
        $ConfigPath = Get-DefaultConfigPath
    }
    $configDir = Split-Path -Parent $ConfigPath
    return Join-Path $configDir 'workspaces.json'
}

function Complete-CdpWorkspaceLaunch {
    param([object[]]$Results, [bool]$HasWindowsTerminal, [switch]$PassThru)

    if (-not $HasWindowsTerminal) {
        Write-Host "`nWindows Terminal (wt.exe) not found. Listed projects above." -ForegroundColor Yellow
        Write-Host 'Install Windows Terminal for multi-tab workspace launching.' -ForegroundColor Gray
    }
    $global:LASTEXITCODE = if (@($Results | Where-Object Status -eq 'failed').Count -gt 0) { 1 } else { 0 }
    if ($PassThru) { $Results }
}

function Invoke-CdpWorkspaceLaunch {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $false)][object[]]$Projects,
        [Parameter(Mandatory = $false)][string[]]$ProjectNames,
        [Parameter(Mandatory = $false)][object]$Launcher,
        [Parameter(Mandatory = $false)][object[]]$Plan,
        [Parameter(Mandatory = $false)][object]$Layout,
        [Parameter(Mandatory = $false)][switch]$PassThru
    )

    if (-not $Layout) { $Layout = [PSCustomObject]@{ mode = 'tabs' } }
    if (-not $Plan) { $Plan = @(New-CdpLegacyWorkspaceLaunchPlan -Projects $Projects -ProjectNames $ProjectNames -Launcher $Launcher) }
    $nativePlan = @(New-CdpWindowsTerminalLaunchPlan -Plan $Plan -Layout $Layout)
    $hasWt = $null -ne (Get-Command wt.exe -ErrorAction SilentlyContinue)
    $results = @()
    $first = $true
    foreach ($nativeItem in $nativePlan) {
        $item = $nativeItem.Item
        if ($item.Status -notin @('ok', 'legacy', 'renamed')) {
            Write-Host " Cannot launch '$($item.Name)': $($item.Status)." -ForegroundColor Yellow
            $results += New-CdpActionResult -Action 'launch-workspace-project' -Target $item.Name -Status 'failed' -Changed $false -Error $item.Status
            continue
        }
        if (-not $hasWt -and -not $WhatIfPreference) {
            Write-Host " $($item.Name) -> $($item.ResolvedPath)" -ForegroundColor Cyan
            $results += New-CdpActionResult -Action 'launch-workspace-project' -Target $item.Name -Status 'skipped' -Changed $false -Error 'Windows Terminal is unavailable.'
            continue
        }

        $wtArgs = if ($first) { $nativeItem.FirstArguments } else { $nativeItem.NextArguments }
        if (-not $PSCmdlet.ShouldProcess("$($item.Name) ($($item.ResolvedPath))", 'Launch Windows Terminal workspace item')) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            $results += New-CdpActionResult -Action 'launch-workspace-project' -Target $item.Name -Status $status -Changed $false
            continue
        }
        try {
            Start-Process wt.exe -ArgumentList $wtArgs -ErrorAction Stop
            Write-Host " Opened workspace item: $($item.Name)" -ForegroundColor Green
            $results += New-CdpActionResult -Action 'launch-workspace-project' -Target $item.Name -Status 'succeeded' -Changed $true
            $first = $false
        } catch {
            Write-Host " Failed to open '$($item.Name)': $($_.Exception.Message)" -ForegroundColor Red
            $results += New-CdpActionResult -Action 'launch-workspace-project' -Target $item.Name -Status 'failed' -Changed $false -Error $_.Exception.Message
        }
    }
    Complete-CdpWorkspaceLaunch -Results $results -HasWindowsTerminal ($hasWt -or $WhatIfPreference) -PassThru:$PassThru
}

function Invoke-CdpWorkspaceAddAction {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param([string]$Name, [string[]]$Projects, [string]$Open, [string]$Layout, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $document = Read-CdpWorkspaceDocument -Path $workspacePath
    $workspaces = @($document.Value)
    if ((Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name).Count -gt 0) {
        throw "Workspace '$Name' already exists."
    }
    $context = Get-CdpWorkspaceConfigContext -ConfigPath $ConfigPath
    $workspace = New-CdpWorkspaceDefinition -Name $Name -ProjectNames $Projects `
        -Projects @($context.Data.Projects) -Open $Open -Layout $Layout
    if (-not $PSCmdlet.ShouldProcess($Name, "Create workspace definition in $workspacePath")) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action add-workspace -Target $Name -Status $status -Changed $false -Details $workspace
        }
        return
    }
    [void](Write-CdpJsonFile -LiteralPath $workspacePath -Value @($workspaces + $workspace) -ExpectedFingerprint $document.Fingerprint)
    Write-Host "Workspace '$Name' created with $($Projects.Count) projects." -ForegroundColor Green
    if ($PassThru) { New-CdpActionResult -Action add-workspace -Target $Name -Status succeeded -Changed $true -Details $workspace }
}

function Invoke-CdpWorkspaceEditAction {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param([string]$Name, [string[]]$Projects, [string]$Open, [string]$Layout, [switch]$ClearOpen, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $document = Read-CdpWorkspaceDocument -Path $workspacePath
    $workspaces = @($document.Value)
    $workspaceMatches = @(Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name)
    if ($workspaceMatches.Count -eq 0) { throw "Workspace '$Name' not found." }
    $context = Get-CdpWorkspaceConfigContext -ConfigPath $ConfigPath
    $update = Update-CdpWorkspaceDefinition -Workspace $workspaceMatches[0] -ProjectNames $Projects `
        -Projects @($context.Data.Projects) -Open $Open -Layout $Layout -ClearOpen:$ClearOpen
    if (-not $update.Changed) {
        if ($PassThru) { return New-CdpActionResult -Action edit-workspace -Target $Name -Status skipped -Changed $false -Details $update.Workspace }
        return
    }
    if (-not $PSCmdlet.ShouldProcess($Name, "Edit workspace definition in $workspacePath")) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action edit-workspace -Target $Name -Status $status -Changed $false -Details $update.Workspace
        }
        return
    }
    [void](Write-CdpJsonFile -LiteralPath $workspacePath -Value $workspaces -ExpectedFingerprint $document.Fingerprint)
    Write-Host "Workspace '$Name' updated." -ForegroundColor Green
    if ($PassThru) { New-CdpActionResult -Action edit-workspace -Target $Name -Status succeeded -Changed $true -Details $update.Workspace }
}

function Invoke-CdpWorkspaceRemoveAction {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param([string]$Name, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $document = Read-CdpWorkspaceDocument -Path $workspacePath
    $workspaces = @($document.Value)
    $workspaceMatches = @(Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name)
    if ($workspaceMatches.Count -eq 0) { throw "Workspace '$Name' not found." }
    if (-not $PSCmdlet.ShouldProcess($Name, "Remove workspace definition from $workspacePath")) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action remove-workspace -Target $Name -Status $status -Changed $false -Details $workspaceMatches[0]
        }
        return
    }
    $remaining = @($workspaces | Where-Object { -not [string]::Equals([string]$_.name, $Name, [StringComparison]::Ordinal) })
    [void](Write-CdpJsonFile -LiteralPath $workspacePath -Value $remaining -ExpectedFingerprint $document.Fingerprint)
    Write-Host "Workspace '$Name' removed." -ForegroundColor Green
    if ($PassThru) { New-CdpActionResult -Action remove-workspace -Target $Name -Status succeeded -Changed $true -Details $workspaceMatches[0] }
}

function Invoke-CdpWorkspaceValidateAction {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param([string]$Name, [switch]$Fix, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $document = Read-CdpWorkspaceDocument -Path $workspacePath
    $workspaces = @($document.Value)
    $targets = if ($Name) { @(Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name) } else { $workspaces }
    if ($Name -and $targets.Count -eq 0) { throw "Workspace '$Name' not found." }
    $context = Get-CdpWorkspaceConfigContext -ConfigPath $ConfigPath
    $results = @()
    foreach ($workspace in $targets) {
        $results += Get-CdpWorkspaceValidation -Workspace $workspace -Projects @($context.Data.Projects)
    }
    if (-not $Fix) { Write-CdpWorkspaceValidation $results; if ($PassThru) { return $results }; return }
    $changed = $false
    foreach ($workspace in $targets) {
        $validation = @(Get-CdpWorkspaceValidation -Workspace $workspace -Projects @($context.Data.Projects))
        $conversion = Convert-CdpWorkspaceReferences -Workspace $workspace -Validation $validation
        if ($conversion.Changed) { $changed = $true }
    }
    Write-CdpWorkspaceValidation $results
    if (-not $changed) {
        if ($PassThru) { return New-CdpActionResult -Action validate-workspace -Target $(if($Name){$Name}else{$workspacePath}) -Status skipped -Changed $false -Details $results }
        return
    }
    if (-not $PSCmdlet.ShouldProcess($workspacePath, 'Migrate resolvable workspace references')) {
        if ($PassThru) {
            $status = if ($WhatIfPreference) { 'preview' } else { 'canceled' }
            return New-CdpActionResult -Action validate-workspace -Target $(if($Name){$Name}else{$workspacePath}) -Status $status -Changed $false -Details $results
        }
        return
    }
    [void](Write-CdpJsonFile -LiteralPath $workspacePath -Value $workspaces -ExpectedFingerprint $document.Fingerprint)
    if ($PassThru) { New-CdpActionResult -Action validate-workspace -Target $(if($Name){$Name}else{$workspacePath}) -Status succeeded -Changed $true -Details $results }
}

function Invoke-CdpWorkspaceShowAction {
    param([string]$Name, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $workspaces = @((Read-CdpWorkspaceDocument -Path $workspacePath).Value)
    $workspaceMatches = @(Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name)
    if ($workspaceMatches.Count -eq 0) { throw "Workspace '$Name' not found." }
    $context = Get-CdpWorkspaceConfigContext -ConfigPath $ConfigPath
    Show-CdpWorkspaceDetail -Workspace $workspaceMatches[0] -Projects @($context.Data.Projects) -PassThru:$PassThru
}

function Invoke-CdpWorkspaceListAction {
    param([string]$ConfigPath)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $workspaces = @((Read-CdpWorkspaceDocument -Path $workspacePath).Value)
    Show-CdpWorkspaceList -Workspaces $workspaces
}

function Invoke-CdpWorkspaceOpenAction {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param([string]$Name, [string]$Open, [string]$ConfigPath, [switch]$PassThru)

    $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath
    $workspaces = @((Read-CdpWorkspaceDocument -Path $workspacePath).Value)
    $workspaceMatches = @(Get-CdpWorkspaceByName -Workspaces $workspaces -Name $Name)
    if ($workspaceMatches.Count -eq 0) { throw "Workspace '$Name' not found." }
    $workspace = $workspaceMatches[0]
    $context = Get-CdpWorkspaceConfigContext -ConfigPath $ConfigPath
    $layout = ConvertTo-CdpWorkspaceLayout -Existing $workspace.layout
    if (-not (Test-CdpWorkspaceLayout -Layout $layout)) { throw 'Workspace layout is invalid.' }
    $plan = @(New-CdpWorkspaceLaunchPlan -Workspace $workspace -Projects @($context.Data.Projects) -OpenOverride $Open)
    $launchParameters = @{ Plan=$plan; Layout=$layout; PassThru=$PassThru }
    if ($WhatIfPreference) { $launchParameters.WhatIf = $true }
    if ($PSBoundParameters.ContainsKey('Confirm')) { $launchParameters.Confirm = $PSBoundParameters['Confirm'] }
    Invoke-CdpWorkspaceLaunch @launchParameters
}

function Invoke-CdpWorkspace {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    param(
        [Parameter(Position = 0)][string]$Name,
        [switch]$List,
        [string]$Add,
        [string]$Show,
        [string]$Edit,
        [string]$Remove,
        [switch]$Validate,
        [switch]$Fix,
        [string[]]$Projects,
        [string]$Open,
        [string]$Layout,
        [switch]$ClearOpen,
        [string]$ConfigPath,
        [switch]$PassThru
    )

    $safety = @{}
    if ($WhatIfPreference) { $safety.WhatIf = $true }
    if ($PSBoundParameters.ContainsKey("Confirm")) { $safety.Confirm = $PSBoundParameters["Confirm"] }
    if ($List) { return Invoke-CdpWorkspaceListAction -ConfigPath $ConfigPath }
    if ($Show) { return Invoke-CdpWorkspaceShowAction -Name $Show -ConfigPath $ConfigPath -PassThru:$PassThru }
    if ($Remove) { return Invoke-CdpWorkspaceRemoveAction @safety -Name $Remove -ConfigPath $ConfigPath -PassThru:$PassThru }
    if ($Validate) {
        return Invoke-CdpWorkspaceValidateAction @safety -Name $Name -Fix:$Fix -ConfigPath $ConfigPath -PassThru:$PassThru
    }
    if ($Edit) {
        return Invoke-CdpWorkspaceEditAction @safety -Name $Edit -Projects $Projects -Open $Open -Layout $Layout `
            -ClearOpen:$ClearOpen -ConfigPath $ConfigPath -PassThru:$PassThru
    }
    if (-not [string]::IsNullOrWhiteSpace($Add)) {
        return Invoke-CdpWorkspaceAddAction @safety -Name $Add -Projects $Projects -Open $Open -Layout $Layout `
            -ConfigPath $ConfigPath -PassThru:$PassThru
    }
    if ([string]::IsNullOrWhiteSpace($Name)) {
        Write-Host "Usage: cdp workspace <name> | cdp workspace list | cdp workspace add <name> <projects...>" -ForegroundColor Yellow
        return
    }
    Invoke-CdpWorkspaceOpenAction @safety -Name $Name -Open $Open -ConfigPath $ConfigPath -PassThru:$PassThru
}

function Invoke-CdpWorkspaceInvocation {
    param([object]$Invocation)

    $safety = @{}
    if ($Invocation.DryRun) { $safety.WhatIf = $true }
    if ($Invocation.Yes) { $safety.Confirm = $false }

    switch ($Invocation.WorkspaceAction) {
        'list' { Invoke-CdpWorkspace -List -ConfigPath $Invocation.ConfigPath; return }
        'show' { Invoke-CdpWorkspace -Show $Invocation.WorkspaceName -ConfigPath $Invocation.ConfigPath; return }
        'add' {
            Invoke-CdpWorkspace @safety `
                -Add $Invocation.WorkspaceName `
                -Projects $Invocation.Projects `
                -Open $Invocation.Open `
                -Layout $Invocation.WorkspaceLayout `
                -ConfigPath $Invocation.ConfigPath
            return
        }
        'edit' {
            Invoke-CdpWorkspace @safety -Edit $Invocation.WorkspaceName -Projects $Invocation.Projects `
                -Open $Invocation.Open -ClearOpen:$Invocation.ClearOpen -Layout $Invocation.WorkspaceLayout `
                -ConfigPath $Invocation.ConfigPath
            return
        }
        'remove' { Invoke-CdpWorkspace @safety -Remove $Invocation.WorkspaceName -ConfigPath $Invocation.ConfigPath; return }
        'validate' {
            Invoke-CdpWorkspace @safety -Validate -Fix:$Invocation.Fix -Name $Invocation.WorkspaceName -ConfigPath $Invocation.ConfigPath
            return
        }
        'open' {
            Invoke-CdpWorkspace @safety `
                -Name $Invocation.WorkspaceName `
                -Open $Invocation.Open `
                -ConfigPath $Invocation.ConfigPath
            return
        }
        default { Invoke-CdpWorkspace -ConfigPath $Invocation.ConfigPath }
    }
}