Public/Config.ps1

# PSSnips — Get-SnipConfig and Set-SnipConfig: read/write module settings.
# Functions to read and write the PSSnips config.json settings file.

function Get-SnipConfig {
    <#
    .SYNOPSIS
        Shows the current PSSnips configuration settings.
 
    .DESCRIPTION
        Reads and displays all settings from the PSSnips config.json file located in
        the ~/.pssnips directory. Settings include the editor command, GitHub token
        and username, snippet storage path, default language, and delete confirmation
        preference. GitHub tokens are masked in the output, showing only the last
        four characters.
 
        Configuration is resolved in priority order (highest first):
          1. Environment variables ($env:PSSNIPS_*)
          2. Workspace config (.pssnips/config.json in cwd, or $env:PSSNIPS_WORKSPACE)
          3. User config (~/.pssnips/config.json)
          4. Module defaults
 
    .PARAMETER ShowSources
        When specified, displays a table showing where each value was resolved from
        (Env / Workspace / User / Default) in addition to the value itself.
 
    .EXAMPLE
        Get-SnipConfig
 
        Displays the full configuration table in the terminal.
 
    .EXAMPLE
        Get-SnipConfig -ShowSources
 
        Displays the configuration with a Source column indicating which layer each
        value was resolved from.
 
    .EXAMPLE
        # Check which editor is configured before editing a snippet
        Get-SnipConfig
        Edit-Snip my-deploy-script
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None. Output is written directly to the host (formatted table).
 
    .NOTES
        Configuration is stored as JSON at ~/.pssnips/config.json.
        Use Set-SnipConfig to change individual settings.
    #>

    [CmdletBinding()]
    param(
        [switch]$ShowSources
    )
    script:InitEnv
    $cfg = script:LoadCfg
    Write-Host ""
    Write-Host " PSSnips Configuration" -ForegroundColor Cyan
    Write-Host " $('─' * 44)" -ForegroundColor DarkGray

    if ($ShowSources) {
        # Build per-key source map
        $defKeys  = @{}; $script:Defaults.GetEnumerator() | ForEach-Object { $defKeys[$_.Key] = $true }
        $userCfg  = @{}
        if (Test-Path $script:CfgFile) {
            try {
                $raw = Get-Content $script:CfgFile -Raw -Encoding UTF8 -ErrorAction Stop
                if ($raw) { ($raw | ConvertFrom-Json -AsHashtable).GetEnumerator() | ForEach-Object { $userCfg[$_.Key] = $true } }
            } catch { Write-Verbose "Get-SnipConfig ShowSources: user config read error — $($_.Exception.Message)" }
        }
        $wsCfg = @{}
        if ($script:WorkspaceCfgFile -and (Test-Path $script:WorkspaceCfgFile)) {
            try {
                $raw = Get-Content $script:WorkspaceCfgFile -Raw -Encoding UTF8 -ErrorAction Stop
                if ($raw) { ($raw | ConvertFrom-Json -AsHashtable).GetEnumerator() | ForEach-Object { $wsCfg[$_.Key] = $true } }
            } catch { Write-Verbose "Get-SnipConfig ShowSources: workspace config read error — $($_.Exception.Message)" }
        }

        Write-Host (" {0,-22} {1,-30} {2}" -f 'Key', 'Value', 'Source') -ForegroundColor DarkGray
        Write-Host (" {0,-22} {1,-30} {2}" -f ('─' * 22), ('─' * 30), ('─' * 10)) -ForegroundColor DarkGray
        foreach ($k in $cfg.Keys) {
            $v = $cfg[$k]
            if ($k -in 'GitHubToken','GitLabToken' -and $v) { $v = '[plain-text]' }
            if ($k -in 'GitHubTokenSecure','GitLabTokenSecure' -and $v) { $v = '[DPAPI encrypted]' }
            if ($k -eq 'BitbucketAppPassword' -and $v) { $v = '[set]' }
            if ($v -is [array]) { $v = $v -join ', ' }
            $envKey = ($script:EnvVarMap.GetEnumerator() | Where-Object { $_.Value -eq $k } | Select-Object -First 1)?.Key
            $src = if ($envKey -and [System.Environment]::GetEnvironmentVariable($envKey)) { 'Env' }
                   elseif ($wsCfg.ContainsKey($k))   { 'Workspace' }
                   elseif ($userCfg.ContainsKey($k))  { 'User' }
                   else                               { 'Default' }
            $srcColor = switch ($src) {
                'Env'       { 'Yellow' }
                'Workspace' { 'Green' }
                'User'      { 'Cyan' }
                default     { 'DarkGray' }
            }
            Write-Host (" {0,-22}" -f $k) -ForegroundColor DarkCyan -NoNewline
            Write-Host (" {0,-30} " -f $v) -NoNewline
            Write-Host $src -ForegroundColor $srcColor
        }
    } else {
        foreach ($k in $cfg.Keys) {
            $v = $cfg[$k]
            if ($k -in 'GitHubToken','GitLabToken' -and $v) { $v = '[plain-text]' }
            if ($k -in 'GitHubTokenSecure','GitLabTokenSecure' -and $v) { $v = '[DPAPI encrypted]' }
            if ($k -eq 'BitbucketAppPassword' -and $v) { $v = '[set]' }
            if ($v -is [array]) { $v = $v -join ', ' }
            Write-Host (" {0,-22}" -f $k) -ForegroundColor DarkCyan -NoNewline
            Write-Host " $v"
        }
    }
    Write-Host ""
}

function Set-SnipConfig {
    <#
    .SYNOPSIS
        Updates one or more PSSnips configuration settings.
 
    .DESCRIPTION
        Loads the current configuration from ~/.pssnips/config.json, applies any
        provided parameter values, and saves the updated configuration back to disk.
        Only the parameters you supply are changed; unspecified settings retain their
        current values. Multiple settings can be updated in a single call.
 
    .PARAMETER Editor
        The command name or path of the preferred text editor (e.g., 'edit', 'nvim',
        'code'). Optional. Falls back through EditorFallbacks if the command is not
        found on PATH.
 
    .PARAMETER GitHubToken
        A GitHub personal access token (PAT) with the 'gist' scope. Optional.
        Required for all Gist operations. Stored in plain text unless -SecureStorage
        is also specified. Token resolution priority at runtime:
          $env:GITHUB_TOKEN > GitHubTokenSecure (DPAPI) > GitHubToken (plain-text)
        WARNING: tokens written to config.json are not encrypted by default.
        Consider using $env:GITHUB_TOKEN for improved security.
 
    .PARAMETER GitLabToken
        A GitLab personal access token with 'api' scope. Optional.
        Required for all GitLab Snippet operations. Stored in plain text unless
        -SecureStorage is also specified. Token resolution priority at runtime:
          $env:GITLAB_TOKEN > GitLabTokenSecure (DPAPI) > GitLabToken (plain-text)
        WARNING: tokens written to config.json are not encrypted by default.
        Consider using $env:GITLAB_TOKEN for improved security.
 
    .PARAMETER BitbucketUsername
        Your Bitbucket username. Optional. Used together with BitbucketAppPassword for
        Basic Auth when calling the Bitbucket Snippets API.
        Falls back to $env:BITBUCKET_USERNAME at runtime.
 
    .PARAMETER BitbucketAppPassword
        A Bitbucket app password with Snippets read/write scope. Optional.
        Required for all Bitbucket Snippet operations. Falls back to
        $env:BITBUCKET_APP_PASSWORD at runtime.
        WARNING: stored in plain text in config.json; prefer the environment variable.
 
    .PARAMETER SecureStorage
        When specified, tokens are encrypted with Windows DPAPI before being written
        to config.json (stored under GitHubTokenSecure / GitLabTokenSecure). DPAPI
        encryption is scoped to the current machine and user account — the encrypted
        value cannot be decrypted on a different machine or by a different user.
        If DPAPI is unavailable, falls back to plain-text storage with a warning.
 
    .PARAMETER GitHubUsername
        Your GitHub username. Optional. Used to list your own Gists when calling
        Get-GistList without specifying -Username.
 
    .PARAMETER SnippetsDir
        Absolute path to the directory where snippet files are stored. Optional.
        Defaults to ~/.pssnips/snippets. The directory is created if it does not exist.
 
    .PARAMETER DefaultLanguage
        The file extension (without dot) used when creating a new snippet without an
        explicit -Language parameter (e.g., 'ps1', 'py', 'js'). Optional.
 
    .PARAMETER ConfirmDelete
        When $true (the default), Remove-Snip prompts for confirmation before
        deleting. Set to $false to suppress the confirmation prompt globally. Optional.
 
    .PARAMETER Scope
        Determines which config file is written. Accepted values:
          User (default) — saves to ~/.pssnips/config.json. Applies to all
                     sessions for the current user.
          Workspace — saves to .pssnips/config.json in the current directory (or the
                     path set in $env:PSSNIPS_WORKSPACE). Workspace settings are
                     project-specific and can be committed to source control.
                     NEVER store secrets (tokens, passwords) in workspace config.
 
    .EXAMPLE
        Set-SnipConfig -Editor nvim
 
        Switches the default editor to Neovim.
 
    .EXAMPLE
        Set-SnipConfig -GitHubToken 'ghp_abc123' -GitHubUsername 'octocat'
 
        Saves GitHub credentials to enable Gist integration (plain-text, with warning).
 
    .EXAMPLE
        Set-SnipConfig -GitHubToken 'ghp_abc123' -SecureStorage
 
        Saves the token encrypted with DPAPI (machine+user scoped).
 
    .EXAMPLE
        Set-SnipConfig -SnippetsDir 'C:\Projects\MySnips' -Scope Workspace
 
        Saves a project-specific snippets directory to the workspace config so it
        only applies when working in the current directory.
 
    .EXAMPLE
        Set-SnipConfig -DefaultLanguage py -ConfirmDelete $false
 
        Sets Python as the default language and disables delete confirmation prompts.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None. Writes a confirmation message to the host on success.
 
    .NOTES
        Settings are persisted to ~/.pssnips/config.json as UTF-8 JSON.
        Use $env:GITHUB_TOKEN or $env:GITLAB_TOKEN for the most secure token handling,
        as environment variables are never written to disk.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'User explicitly opts in to DPAPI encryption; ConvertTo-SecureString required as first step.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '',
        Justification = 'BitbucketUsername + BitbucketAppPassword are config setters, not authentication parameters.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '',
        Justification = 'BitbucketAppPassword is a config setter; plain string is required for CLI input.')]
    param(
        [ValidateNotNullOrEmpty()][string]$Editor,
        [ValidateNotNullOrEmpty()][string]$GitHubToken,
        [ValidateNotNullOrEmpty()][string]$GitLabToken,
        [ValidateNotNullOrEmpty()][string]$GitHubUsername,
        [ValidateNotNullOrEmpty()][string]$SnippetsDir,
        [ValidateNotNullOrEmpty()][string]$DefaultLanguage,
        [nullable[bool]]$ConfirmDelete,
        [ValidateNotNullOrEmpty()][string]$BitbucketUsername,
        [ValidateNotNullOrEmpty()][string]$BitbucketAppPassword,
        [switch]$SecureStorage,
        [ValidateSet('User','Workspace')][string]$Scope = 'User'
    )
    script:InitEnv
    $cfg = script:LoadCfg
    if ($Editor)          { $cfg['Editor']          = $Editor          }
    if ($PSBoundParameters.ContainsKey('GitHubToken')) {
        Write-Warning "GitHub tokens stored in config.json are not encrypted. Consider using `$env:GITHUB_TOKEN instead."
        if ($SecureStorage) {
            try {
                $cfg['GitHubTokenSecure'] = $GitHubToken | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
                $cfg.Remove('GitHubToken')
            } catch {
                Write-Warning "DPAPI encryption failed; falling back to plain-text storage. Error: $($_.Exception.Message)"
                $cfg['GitHubToken'] = $GitHubToken
            }
        } else {
            $cfg['GitHubToken'] = $GitHubToken
        }
    }
    if ($PSBoundParameters.ContainsKey('GitLabToken')) {
        Write-Warning "GitLab tokens stored in config.json are not encrypted. Consider using `$env:GITLAB_TOKEN instead."
        if ($SecureStorage) {
            try {
                $cfg['GitLabTokenSecure'] = $GitLabToken | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
                $cfg.Remove('GitLabToken')
            } catch {
                Write-Warning "DPAPI encryption failed; falling back to plain-text storage. Error: $($_.Exception.Message)"
                $cfg['GitLabToken'] = $GitLabToken
            }
        } else {
            $cfg['GitLabToken'] = $GitLabToken
        }
    }
    if ($GitHubUsername)       { $cfg['GitHubUsername']       = $GitHubUsername       }
    if ($SnippetsDir)          { $cfg['SnippetsDir']          = $SnippetsDir          }
    if ($DefaultLanguage)      { $cfg['DefaultLanguage']      = $DefaultLanguage      }
    if ($null -ne $ConfirmDelete) { $cfg['ConfirmDelete']     = $ConfirmDelete        }
    if ($BitbucketUsername)    { $cfg['BitbucketUsername']    = $BitbucketUsername    }
    if ($BitbucketAppPassword) { $cfg['BitbucketAppPassword'] = $BitbucketAppPassword }
    if ($PSCmdlet.ShouldProcess("$Scope config", 'Save configuration')) {
        script:SaveCfg -Cfg $cfg -Scope $Scope
        script:Out-OK "Configuration saved."
    }
}