Private/ProfileStore.ps1

# The profile registry: ccs.json. Small on purpose — it stores only what cannot
# be derived. Backend (subscription vs bedrock) is always read from the profile's
# own settings.json so the registry can never drift out of sync with reality.

$script:CcsConfigVersion = 1

function New-CcsConfig {
    <#
    .SYNOPSIS
        A fresh registry pre-seeded with the built-in 'personal' profile (~/.claude).
    #>

    [pscustomobject]@{
        version  = $script:CcsConfigVersion
        profiles = [pscustomobject]@{
            personal = [pscustomobject]@{
                dir  = Resolve-CcsConfigDirValue (Get-CcsDefaultClaudeHome)
                note = 'Default Claude Code config home'
            }
        }
    }
}

function Get-CcsConfig {
    <#
    .SYNOPSIS
        Read the registry, returning a seeded default when it does not exist yet.
    #>

    $config = Read-CcsJson -Path (Get-CcsConfigPath)
    if (-not $config) { return New-CcsConfig }
    if (-not $config.PSObject.Properties['profiles']) {
        throw "Registry at $(Get-CcsConfigPath) has no 'profiles' section"
    }
    $config
}

function Save-CcsConfig {
    param([Parameter(Mandatory)]$Config)
    Write-CcsJson -Path (Get-CcsConfigPath) -Value $Config
}

function Add-CcsConfigProfile {
    <#
    .SYNOPSIS
        Return a NEW registry with one profile added or replaced.
    #>

    param(
        [Parameter(Mandatory)]$Config,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$Dir,
        [string]$Note = ''
    )

    $profiles = [pscustomobject]@{}
    foreach ($p in $Config.profiles.PSObject.Properties) {
        if ($p.Name -eq $Name) { continue }
        Add-Member -InputObject $profiles -NotePropertyName $p.Name -NotePropertyValue $p.Value
    }
    Add-Member -InputObject $profiles -NotePropertyName $Name -NotePropertyValue ([pscustomobject]@{
        dir  = Resolve-CcsConfigDirValue $Dir
        note = $Note
    })

    [pscustomobject]@{
        version  = $script:CcsConfigVersion
        profiles = $profiles
    }
}

function Remove-CcsConfigProfile {
    <#
    .SYNOPSIS
        Return a NEW registry without the named profile.
    #>

    param(
        [Parameter(Mandatory)]$Config,
        [Parameter(Mandatory)][string]$Name
    )

    $profiles = [pscustomobject]@{}
    foreach ($p in $Config.profiles.PSObject.Properties) {
        if ($p.Name -eq $Name) { continue }
        Add-Member -InputObject $profiles -NotePropertyName $p.Name -NotePropertyValue $p.Value
    }
    [pscustomobject]@{ version = $script:CcsConfigVersion; profiles = $profiles }
}

function Resolve-CcsProfile {
    <#
    .SYNOPSIS
        Look up one profile by name and describe it, reading its live settings.
    .OUTPUTS
        Name, Dir, Note, Exists, Backend ('bedrock'|'subscription'|'unknown'),
        Settings (parsed settings.json or $null), Env (its env block as a hashtable).
    #>

    param(
        [Parameter(Mandatory)][string]$Name,
        $Config = (Get-CcsConfig)
    )

    $entry = $Config.profiles.PSObject.Properties[$Name]
    if (-not $entry) {
        $known = ($Config.profiles.PSObject.Properties.Name | Sort-Object) -join ', '
        throw "Unknown profile '$Name'. Known profiles: $known"
    }

    $dir = $entry.Value.dir
    $settingsPath = Get-CcsSettingsPath -ProfileDir $dir
    $settings = $null
    try { $settings = Read-CcsJson -Path $settingsPath } catch { Write-Warning $_.Exception.Message }

    $backend = 'unknown'
    if ($settings) { $backend = if (Test-CcsBedrockSettings -Settings $settings) { 'bedrock' } else { 'subscription' } }

    [pscustomobject]@{
        Name         = $Name
        Dir          = $dir
        Note         = $entry.Value.note
        Exists       = Test-Path -LiteralPath $dir
        SettingsPath = $settingsPath
        Settings     = $settings
        Env          = Get-CcsSettingsEnv -Settings $settings
        Backend      = $backend
    }
}

function Get-CcsActiveProfileName {
    <#
    .SYNOPSIS
        Which profile this shell is currently pointed at, by CLAUDE_CONFIG_DIR.
    .DESCRIPTION
        An unset CLAUDE_CONFIG_DIR means Claude Code uses ~/.claude, so it maps to
        whichever profile registers that directory. Returns $null when the env var
        points somewhere no profile claims.
    #>

    param($Config = (Get-CcsConfig))

    $current = $env:CLAUDE_CONFIG_DIR
    if ([string]::IsNullOrWhiteSpace($current)) { $current = Get-CcsDefaultClaudeHome }
    $normalized = Resolve-CcsConfigDirValue $current

    foreach ($p in $Config.profiles.PSObject.Properties) {
        if ($p.Value.dir -eq $normalized) { return $p.Name }
    }
    $null
}