Public/Profiles.ps1

# Profile lifecycle: list, capture, use, run.

function Get-CcsProfileList {
    <#
    .SYNOPSIS
        Every registered profile with its backend and active state.
    #>

    [CmdletBinding()]
    param()

    $config = Get-CcsConfig
    $active = Get-CcsActiveProfileName -Config $config

    foreach ($p in $config.profiles.PSObject.Properties) {
        $resolved = Resolve-CcsProfile -Name $p.Name -Config $config
        [pscustomobject]@{
            PSTypeName = 'Ccs.Profile'
            Active  = ($p.Name -eq $active)
            Name    = $p.Name
            Backend = $resolved.Backend
            Model   = if ($resolved.Settings) { $resolved.Settings.model } else { $null }
            Dir     = $resolved.Dir
            Exists  = $resolved.Exists
            Note    = $resolved.Note
        }
    }
}

function New-CcsProfileFromCurrent {
    <#
    .SYNOPSIS
        Capture the settings currently in ~/.claude into a new named profile.
    .DESCRIPTION
        Copies settings.json (and, unless -SettingsOnly, CLAUDE.md plus the
        commands/agents/skills folders) into a fresh config home, then registers it.
        Credentials are never copied: a Bedrock profile authenticates through AWS,
        and a subscription profile logs in on first use.
    .PARAMETER Name
        Profile name, used as the directory name under ~/.claude-profiles.
    .PARAMETER From
        Source config home. Defaults to Claude Code's built-in ~/.claude.
    .PARAMETER FromSettingsFile
        Take settings.json from this exact file instead of $From\settings.json —
        for building a profile out of a settings backup you already kept aside.
    .PARAMETER LinkShared
        Junction commands/agents/skills back to the source profile (and hard-link
        CLAUDE.md) instead of copying, so both profiles share one set.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Name,
        [string]$From = (Get-CcsDefaultClaudeHome),
        [string]$FromSettingsFile,
        [string]$Note = '',
        [switch]$SettingsOnly,
        [switch]$LinkShared,
        [switch]$Force
    )

    Assert-CcsProfileName -Name $Name

    $target = Get-CcsProfileDir -Name $Name
    if ((Test-Path -LiteralPath $target) -and -not $Force) {
        throw "Profile directory already exists: $target (use -Force to overwrite its settings)"
    }

    $sourceSettings = if ($FromSettingsFile) { $FromSettingsFile } else { Get-CcsSettingsPath -ProfileDir $From }
    if (-not (Test-Path -LiteralPath $sourceSettings)) {
        throw "No settings file found at $sourceSettings — nothing to capture"
    }
    # Fail before creating anything if the source is not valid JSON.
    Read-CcsJson -Path $sourceSettings | Out-Null

    if (-not $PSCmdlet.ShouldProcess($target, "Capture profile '$Name' from $From")) { return }

    New-Item -ItemType Directory -Path $target -Force | Out-Null
    $targetSettings = Get-CcsSettingsPath -ProfileDir $target
    Backup-CcsFile -Path $targetSettings | Out-Null
    Copy-Item -LiteralPath $sourceSettings -Destination $targetSettings -Force

    if (-not $SettingsOnly) {
        foreach ($item in @('CLAUDE.md', 'commands', 'agents', 'skills')) {
            $src = Join-Path $From $item
            if (-not (Test-Path -LiteralPath $src)) { continue }
            $dst = Join-Path $target $item

            if ($LinkShared -and (Get-Item -LiteralPath $src).PSIsContainer) {
                # A junction keeps one copy of agents/skills/commands for every
                # profile — edit them once, both backends see it. Junctions and
                # hard links need no elevation; symlinks would.
                if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Recurse -Force }
                New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
            } elseif ($LinkShared) {
                if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }
                New-Item -ItemType HardLink -Path $dst -Target $src | Out-Null
            } else {
                Copy-Item -LiteralPath $src -Destination $dst -Recurse -Force
            }
        }
    }

    $config = Add-CcsConfigProfile -Config (Get-CcsConfig) -Name $Name -Dir $target -Note $Note
    Save-CcsConfig -Config $config

    Resolve-CcsProfile -Name $Name -Config $config
}

function Remove-CcsProfileEntry {
    <#
    .SYNOPSIS
        Unregister a profile. Its directory is kept unless -DeleteFiles is given.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Name,
        [switch]$DeleteFiles
    )

    $config = Get-CcsConfig
    $resolved = Resolve-CcsProfile -Name $Name -Config $config
    if ($resolved.Dir -eq (Resolve-CcsConfigDirValue (Get-CcsDefaultClaudeHome))) {
        throw "Refusing to remove the profile that points at Claude Code's default config home"
    }
    if (-not $PSCmdlet.ShouldProcess($Name, 'Remove profile')) { return }

    Save-CcsConfig -Config (Remove-CcsConfigProfile -Config $config -Name $Name)
    if ($DeleteFiles -and (Test-Path -LiteralPath $resolved.Dir)) {
        Remove-Item -LiteralPath $resolved.Dir -Recurse -Force
    }
}

function Use-CcsProfile {
    <#
    .SYNOPSIS
        Point THIS shell at a profile by setting CLAUDE_CONFIG_DIR.
    .DESCRIPTION
        Only the current session is affected — other terminals, the VS Code
        extension and Claude Desktop keep their own profile, so two backends can
        run side by side. Auth-override variables that would hijack the session
        are cleared with a warning.
    #>

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

    $resolved = Resolve-CcsProfile -Name $Name
    if (-not $resolved.Exists) {
        throw "Profile '$Name' points at $($resolved.Dir), which does not exist. Run 'ccs capture' first."
    }

    foreach ($var in $script:AuthOverrideEnvVars) {
        if (Test-Path "env:$var") {
            Write-Warning "Clearing $var in this shell — it would override profile '$Name'"
            Remove-Item "env:$var"
        }
    }

    $env:CLAUDE_CONFIG_DIR = $resolved.Dir
    Write-Verbose "CLAUDE_CONFIG_DIR = $($resolved.Dir)"
    $resolved
}

function Start-CcsClaude {
    <#
    .SYNOPSIS
        Launch Claude Code under a profile without changing this shell.
    .PARAMETER ClaudeArgs
        Arguments forwarded to claude, e.g. 'ccs run work -- --resume'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)][string]$Name,
        [Parameter(ValueFromRemainingArguments)][string[]]$ClaudeArgs
    )

    $resolved = Resolve-CcsProfile -Name $Name
    if (-not $resolved.Exists) {
        throw "Profile '$Name' points at $($resolved.Dir), which does not exist. Run 'ccs capture' first."
    }

    $claude = Get-Command claude -ErrorAction SilentlyContinue
    if (-not $claude) { throw "claude was not found on PATH" }

    $saved = @{}
    foreach ($var in @('CLAUDE_CONFIG_DIR') + $script:AuthOverrideEnvVars) {
        $saved[$var] = [Environment]::GetEnvironmentVariable($var)
    }
    try {
        foreach ($var in $script:AuthOverrideEnvVars) {
            if (Test-Path "env:$var") {
                Write-Warning "Ignoring $var for this launch — it would override profile '$Name'"
                Remove-Item "env:$var"
            }
        }
        $env:CLAUDE_CONFIG_DIR = $resolved.Dir
        & $claude.Source @ClaudeArgs
    } finally {
        foreach ($var in $saved.Keys) {
            if ($null -eq $saved[$var]) { Remove-Item "env:$var" -ErrorAction SilentlyContinue }
            else { Set-Item "env:$var" -Value $saved[$var] }
        }
    }
}