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] } } } } |