cd /news/developer-tools/powershell-profile-and-test-suite · home topics developer-tools article
[ARTICLE · art-49233] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

PowerShell profile and test suite

Matthew created a production-ready PowerShell prompt that supports PS 5.1 and PS 7+ with segments for Git, Python venv, Conda, Node, .NET SDK, exit code, jobs, admin, Kubernetes, Azure/AWS/GCP, depth, path, and timestamp. The prompt uses a Campbell palette with 24-bit ANSI for PS7 and ConsoleColor fallback for PS5, and includes environment capability probing to adapt to constrained language mode, non-interactive hosts, and ANSI support.

read26 min views1 publishedJul 7, 2026

| #Requires -Version 5.1 | | | | | | | | | | | | | $script:PS7 = $PSVersionTable.PSVersion.Major -ge 7 | | | | | | | | | | | | | | | | $script:ConstrainedLang = $false | | try { $script:ConstrainedLang = $ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' } catch { } | | | | | | | | | $script:IsInteractive = $true | | try { | | if (-not [Environment]::UserInteractive) { $script:IsInteractive = $false } | | elseif (([Environment]::GetCommandLineArgs()) -match '^-NonInteractive') { $script:IsInteractive = $false } | | } catch { } | | | | | | | $script:SupportsAnsi = $false | | try { $script:SupportsAnsi = $script:PS7 -and [bool]$Host.UI.SupportsVirtualTerminal } catch { } | | | | | $script:SupportsTitle = $false | | try { $null = $Host.UI.RawUI.WindowTitle; $script:SupportsTitle = $true } catch { } | | | | | | | | | | | | if ($script:SupportsAnsi) { | | | $E = [char]27 # ESC character | | | | | function script:Fg { param([int]$R,[int]$G,[int]$B) "$E[38;2;$R;$G;${B}m" } | | function script:Bg { param([int]$R,[int]$G,[int]$B) "$E[48;2;$R;$G;${B}m" } | | $script:Reset = "$E[0m" | | $script:Bold = "$E[1m" | | $script:Dim = "$E[2m" | | | | | $script:CBlack = Fg 12 12 12 # Black | | $script:CRed = Fg 197 15 31 # Red | | $script:CGreen = Fg 19 161 14 # Green | | $script:CYellow = Fg 193 156 0 # Yellow | | $script:CBlue = Fg 0 55 218 # Blue | | $script:CMagenta = Fg 136 0 128 # Magenta | | $script:CCyan = Fg 58 150 221 # Cyan | | $script:CWhite = Fg 204 204 204 # White (light gray) | | $script:CBrBlack = Fg 118 118 118 # Bright Black (dark gray) | | $script:CBrRed = Fg 231 72 86 # Bright Red | | $script:CBrGreen = Fg 22 198 12 # Bright Green | | $script:CBrYellow= Fg 249 241 165 # Bright Yellow | | $script:CBrBlue = Fg 59 120 255 # Bright Blue | | $script:CBrMag = Fg 180 0 158 # Bright Magenta | | $script:CBrCyan = Fg 97 214 214 # Bright Cyan | | $script:CBrWhite = Fg 242 242 242 # Bright White | | | | | $script:ColPath = $script:CBrCyan | | $script:ColGitBranch= $script:CBrMag | | $script:ColGitDirty = $script:CBrRed | | $script:ColGitClean = $script:CBrGreen | | $script:ColGitInfo = $script:CYellow | | $script:ColPython = $script:CBrYellow | | $script:ColConda = $script:CYellow | | $script:ColNode = $script:CGreen | | $script:ColDotNet = $script:CBrBlue | | $script:ColErrCode = $script:CBrRed | | $script:ColOK = $script:CBrGreen | | $script:ColAdmin = $script:CBrRed | | $script:ColJobs = $script:CCyan | | $script:ColK8s = $script:CBrBlue | | $script:ColCloud = $script:CBrCyan | | $script:ColDepth = $script:CBrBlack | | $script:ColTime = $script:CBrBlack | | $script:ColSep = $script:CBrBlack | | $script:ColPromptOK = $script:CBrGreen | | $script:ColPromptErr= $script:CBrRed | | | | | function script:Styled { | | param([string]$Color, [string]$Text, [switch]$Bold) | | if ($Bold) { return "$script:Bold$Color$Text$script:Reset" } | | return "$Color$Text$script:Reset" | | } | | | | } else { | | | | | | | $script:ColPath = 'Cyan' | | $script:ColGitBranch = 'Magenta' | | $script:ColGitDirty = 'Red' | | $script:ColGitClean = 'Green' | | $script:ColGitInfo = 'Yellow' | | $script:ColPython = 'Yellow' | | $script:ColConda = 'Yellow' | | $script:ColNode = 'Green' | | $script:ColDotNet = 'Blue' | | $script:ColErrCode = 'Red' | | $script:ColOK = 'Green' | | $script:ColAdmin = 'Red' | | $script:ColJobs = 'Cyan' | | $script:ColK8s = 'Blue' | | $script:ColCloud = 'Cyan' | | $script:ColDepth = 'DarkGray' | | $script:ColTime = 'DarkGray' | | $script:ColSep = 'DarkGray' | | $script:ColPromptOK = 'Green' | | $script:ColPromptErr = 'Red' | | | | | $script:PromptPieces = [System.Collections.Generic.List[hashtable]]::new() | | | | function script:Styled { | | param([string]$Color, [string]$Text, [switch]$Bold) | | | $null = $script:PromptPieces.Add(@{ Color = $Color; Text = $Text }) | | return '' | | } | | } | | | | | | | | | | $script:Sep = '|' | | | | | | | | | $script:PathComparison = [System.StringComparison]::OrdinalIgnoreCase | | if ($script:PS7 -and -not $IsWindows) { | | $script:PathComparison = [System.StringComparison]::Ordinal | | } | | | | | function script:Seg { | | param([string]$Color, [string]$Text) | | if ($script:SupportsAnsi) { | | return "$(Styled $Color "$Text")$( Styled $script:ColSep $script:Sep )" | | } else { | | $null = $script:PromptPieces.Add(@{ Color = $Color; Text = "$Text" }) | | $null = $script:PromptPieces.Add(@{ Color = $script:ColSep; Text = $script:Sep }) | | return '' | | } | | } | | | | | | | | | | $script:CmdCache = @{} | | | | | $script:NodeCache = @{} | | $script:DotNetCache = @{} | | function script:CommandExists { | | param([string]$cmd) | | if (-not $script:CmdCache.ContainsKey($cmd)) { | | $script:CmdCache[$cmd] = [bool](Get-Command $cmd -ErrorAction Ignore) | | } | | return $script:CmdCache[$cmd] | | } | | | | | function script:TruncatePath { | | param([string]$FullPath, [int]$MaxDepth = 4) | | $home_path = $HOME -replace '\','/' | | $display = $FullPath -replace '\','/' | | | | if ($home_path -and $display.StartsWith($home_path, $script:PathComparison)) { | | $display = '~' + $display.Substring($home_path.Length) | | } | | $parts = $display.TrimStart('/').Split('/') | | if ($display.StartsWith('~')) { | | | | $rest = if ($display.Length -gt 2) { $display.Substring(2) } else { '' } | | $parts = @('~') + ($rest.Split('/') | Where-Object { $_ }) | | } | | $depth = $parts.Count | | if ($depth -le $MaxDepth) { return $display, $depth } | | | | | $tail = $parts | Select-Object -Last ($MaxDepth - 1) | | $truncated = '…/' + ($tail -join '/') | | return $truncated, $depth | | } | | | | | | | | | | function script:Get-AdminSegment { | | $isAdmin = $false | | if ($script:PS7 -and $IsLinux) { | | $isAdmin = (id -u) -eq '0' | | } else { | | $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() | | $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | | } | | if ($isAdmin) { return Seg $script:ColAdmin '⚡ADMIN' } | | return '' | | } | | | | | function script:Get-ExitCodeSegment { | | param([int]$LastExit) | | if ($LastExit -ne 0) { | | return Seg $script:ColErrCode "✘ $LastExit" | | } | | return '' | | } | | | | | function script:Get-JobsSegment { | | $jobs = @(Get-Job -ErrorAction SilentlyContinue | Where-Object { $.State -ne 'Completed' -and $.State -ne 'Failed' }) | | if ($jobs.Count -gt 0) { | | return Seg $script:ColJobs "⚙ $($jobs.Count)" | | } | | return '' | | } | | | | | function script:Get-PathSegment { | | $raw = $PWD.ProviderPath | | $display, $depth = TruncatePath $raw 4 | | | | | $depthStr = '' | | if ($depth -gt 3) { | | if ($script:SupportsAnsi) { $depthStr = "$(Styled $script:ColDepth "[$depth]")" } | | else { | | $null = $script:PromptPieces.Add(@{ Color = $script:ColDepth; Text = "[$depth]" }) | | } | | } | | | | $pathStr = Seg $script:ColPath $display | | if ($script:SupportsAnsi) { return "$depthStr$pathStr" } | | return '' | | } | | | | | | | | | $script:GitShowExtras = $false | | | | function script:Get-GitSegment { | | if (-not (CommandExists 'git')) { return '' } | | | | | | | | $statusOut = git status --porcelain=v2 --branch 2>$null | | if ($LASTEXITCODE -ne 0) { return '' } | | | | $branch = ''; $oid = ''; $ahead = 0; $behind = 0 | | $xy = [System.Collections.Generic.List[string]]::new() | | foreach ($line in $statusOut) { | | if (-not $line) { continue } | | $c = $line[0] | | if ($c -eq '#') { | | if ($line.StartsWith('# branch.head ')) { $branch = $line.Substring(14) } | | elseif ($line.StartsWith('# branch.oid ')) { $oid = $line.Substring(13) } | | elseif ($line.StartsWith('# branch.ab ') -and $line -match '+(\d+)\s+-(\d+)') { | | $ahead = [int]$Matches[1]; $behind = [int]$Matches[2] | | } | | } | | | | elseif (($c -eq '1' -or $c -eq '2') -and $line.Length -ge 4) { $xy.Add(($line.Substring(2,2) -replace '.',' ')) } | | elseif ($c -eq 'u' -and $line.Length -ge 4) { $xy.Add($line.Substring(2,2)) } # unmerged | | elseif ($c -eq '?') { $xy.Add('??') } # untracked | | } | | | | | if ($branch -eq '(detached)') { | | $branch = if ($oid.Length -ge 7) { "➦ $($oid.Substring(0,7))" } else { '➦ ?' } | | } | | | | | | $dirty = @($xy | Where-Object { $_ -match '[1][MADRCU?! ]' }).Count -gt 0 | | $staged = @($xy | Where-Object { $_ -match '[2]' }).Count -gt 0 | | | | | | | $stashCount = 0 | | $worktreeStr = '' | | if ($script:GitShowExtras) { | | $stashCount = (git stash list 2>$null | Measure-Object).Count | | $worktreeCount = (git worktree list --porcelain 2>$null | Where-Object { $_ -match '^worktree ' } | Measure-Object).Count | | if ($worktreeCount -gt 1) { $worktreeStr = " ⎇$worktreeCount" } | | } | | | | $branchColor = if ($dirty -or $staged) { $script:ColGitDirty } else { $script:ColGitClean } | | $gitStr = "$branch$worktreeStr" | | | | $suffix = '' | | if ($staged) { $suffix += ' ●' } # staged changes | | if ($dirty) { $suffix += ' ✚' } # unstaged changes | | if ($ahead -gt 0) { $suffix += " ↑$ahead" } # ahead of remote | | if ($behind -gt 0) { $suffix += " ↓$behind" } # behind remote | | if ($stashCount -gt 0) { $suffix += " ≡$stashCount" } # stashes | | | | return Seg $branchColor "$gitStr$suffix" | | } | | | | | function script:Get-PythonVenvSegment { | | $venv = $env:VIRTUAL_ENV | | if (-not $venv) { return '' } | | $venvName = Split-Path $venv -Leaf | | return Seg $script:ColPython "🐍 $venvName" | | } | | | | | function script:Get-CondaSegment { | | $condaEnv = $env:CONDA_DEFAULT_ENV | | if (-not $condaEnv -or $condaEnv -eq 'base') { return '' } | | return Seg $script:ColConda "🅒 $condaEnv" | | } | | | | | function script:Get-NodeSegment { | | if (-not (CommandExists 'node')) { return '' } | | | | $cwd = $PWD.ProviderPath | | $now = [datetime]::UtcNow | | $entry = $script:NodeCache[$cwd] | | if (-not $entry -or $now -ge $entry.Exp) { | | $entry = @{ Ver = (Find-NodeVersion $cwd); Exp = $now.AddSeconds(30) } | | $script:NodeCache[$cwd] = $entry | | } | | if ($entry.Ver) { return Seg $script:ColNode "⬡ $($entry.Ver)" } | | return '' | | } | | | | | function script:Find-NodeVersion { | | param([string]$startDir) | | $markers = @('package.json','.nvmrc','.node-version') | | $check = $startDir | | while ($check -and $check -ne (Split-Path $check -Parent)) { | | foreach ($m in $markers) { | | if (Test-Path (Join-Path $check $m)) { | | $v = node --version 2>$null | | return $(if ($v) { "$v" } else { $null }) | | } | | } | | $check = Split-Path $check -Parent | | } | | return $null | | } | | | | | function script:Get-DotNetSegment { | | if (-not (CommandExists 'dotnet')) { return '' } | | | | | $cwd = $PWD.ProviderPath | | $now = [datetime]::UtcNow | | $entry = $script:DotNetCache[$cwd] | | if (-not $entry -or $now -ge $entry.Exp) { | | $entry = @{ Ver = (Find-DotNetVersion $cwd); Exp = $now.AddSeconds(30) } | | $script:DotNetCache[$cwd] = $entry | | } | | if ($entry.Ver) { return Seg $script:ColDotNet ".NET $($entry.Ver)" } | | return '' | | } | | | | | | | function script:Find-DotNetVersion { | | param([string]$startDir) | | $check = $startDir | | while ($check -and $check -ne (Split-Path $check -Parent)) { | | if ((Test-Path (Join-Path $check 'global.json')) -or | | (Get-ChildItem -Path (Join-Path $check '') -Include '.csproj','.fsproj','.vbproj','*.sln' -File -ErrorAction Ignore | Select-Object -First 1)) { | | $v = dotnet --version 2>$null | | return $(if ($v) { "$v" } else { $null }) | | } | | $check = Split-Path $check -Parent | | } | | return $null | | } | | | | | function script:Get-K8sSegment { | | if (-not (CommandExists 'kubectl')) { return '' } | | | | | | | | | | $now = [datetime]::UtcNow | | if (-not $script:K8sCacheExpiry -or $now -ge $script:K8sCacheExpiry) { | | $script:K8sCtx = kubectl config current-context 2>$null | | $script:K8sNs = kubectl config view --minify --output 'jsonpath={..namespace}' 2>$null | | $script:K8sCacheExpiry = $now.AddSeconds(30) | | } | | | | if ($script:K8sCtx) { | | $nsStr = if ($script:K8sNs -and $script:K8sNs -ne 'default') { ":$($script:K8sNs)" } else { '' } | | return Seg $script:ColK8s "☸ $($script:K8sCtx)$nsStr" | | } | | return '' | | } | | | | | function script:Get-CloudSegment { | | $parts = [System.Collections.Generic.List[string]]::new() | | | | | if (CommandExists 'az') { | | | | $azProfile = [IO.Path]::Combine($HOME, '.azure', 'azureProfile.json') | | if (Test-Path $azProfile) { | | try { | | $azData = Get-Content $azProfile -Raw | ConvertFrom-Json | | $sub = $azData.subscriptions | Where-Object { $_.isDefault } | Select-Object -First 1 | | if ($sub) { $parts.Add("☁ az:$($sub.name)") } | | } catch { } | | } | | } | | | | | if (CommandExists 'aws') { | | $awsProfile = $env:AWS_PROFILE | | if (-not $awsProfile) { $awsProfile = $env:AWS_DEFAULT_PROFILE } | | if (-not $awsProfile) { $awsProfile = 'default' } | | if ($awsProfile -ne 'default') { $parts.Add("aws:$awsProfile") } | | } | | | | | if (CommandExists 'gcloud') { | | | $gcpConfig = [IO.Path]::Combine($HOME, '.config', 'gcloud', 'active_config') | | if (Test-Path $gcpConfig) { | | $cfg = (Get-Content $gcpConfig -ErrorAction SilentlyContinue).Trim() | | if ($cfg) { | | | | | | $now = [datetime]::UtcNow | | if ($script:GcpProjConfig -ne $cfg -or -not $script:GcpProjExpiry -or $now -ge $script:GcpProjExpiry) { | | $script:GcpProj = gcloud config get-value project 2>$null | | $script:GcpProjConfig = $cfg | | $script:GcpProjExpiry = $now.AddSeconds(30) | | } | | if ($script:GcpProj) { $parts.Add("gcp:$($script:GcpProj)") } else { $parts.Add("gcp:$cfg") } | | } | | } | | } | | | | if ($parts.Count -eq 0) { return '' } | | return Seg $script:ColCloud ($parts -join ' ') | | } | | | | | function script:Get-TimeSegment { | | return Seg $script:ColTime (Get-Date -Format 'HH:mm:ss') | | } | | | | | | | | | | | function script:Get-RichPrompt { | | param($RealLastExit) | | | | | $lastExit = if ($null -ne $RealLastExit) { $RealLastExit } else { 0 } | | | | if (-not $script:SupportsAnsi) { | | | $script:PromptPieces = [System.Collections.Generic.List[hashtable]]::new() | | } | | | | | $line1 = '' | | | | | $line1 += Get-AdminSegment | | $line1 += Get-ExitCodeSegment -LastExit $lastExit | | $line1 += Get-JobsSegment | | $line1 += Get-PathSegment | | $line1 += Get-GitSegment | | $line1 += Get-PythonVenvSegment | | $line1 += Get-CondaSegment | | $line1 += Get-NodeSegment | | $line1 += Get-DotNetSegment | | $line1 += Get-CloudSegment | | $line1 += Get-K8sSegment | | $line1 += Get-TimeSegment | | | | | $glyphColor = if ($lastExit -eq 0) { $script:ColPromptOK } else { $script:ColPromptErr } | | $nestedDepth = $NestedPromptLevel | | $glyph = ('❯' * [Math]::Max(1, $nestedDepth + 1)) | | | | | | | | $global:LASTEXITCODE = $RealLastExit | | | | if ($script:SupportsAnsi) { | | | return "n$line1 n$(Styled $glyphColor $glyph) " | | } else { | | | Write-Host "n" -NoNewline | | foreach ($piece in $script:PromptPieces) { | | Write-Host $piece.Text -ForegroundColor $piece.Color -NoNewline | | } | | Write-Host "n" -NoNewline | | Write-Host $glyph -ForegroundColor $glyphColor -NoNewline | | return ' ' | | } | | } | | | | | | | | function script:Get-MinimalPrompt { | | param($RealLastExit) | | $loc = (Get-Location).Path | | $code = if ($RealLastExit -and $RealLastExit -ne 0) { " [$RealLastExit]" } else { '' } | | $depth = '>' * (1 + $NestedPromptLevel) | | return "PS $loc$code$depth " | | } | | | | function global:prompt { | | | | | | | $realLastExit = $global:LASTEXITCODE | | $errMark = $global:Error.Count | | try { | | if ($script:SupportsTitle) { Set-PromptTitle } | | if ($script:ConstrainedLang) { return Get-MinimalPrompt $realLastExit } | | return Get-RichPrompt $realLastExit | | } catch { | | | return Get-MinimalPrompt $realLastExit | | } finally { | | $global:LASTEXITCODE = $realLastExit | | | | while ($global:Error.Count -gt $errMark) { $global:Error.RemoveAt(0) } | | } | | } | | | | | | | | | | | | function script:Set-PromptTitle { | | $location = $PWD.ProviderPath | | $home_path = $HOME | | if ($home_path -and $location.StartsWith($home_path, $script:PathComparison)) { | | $location = '~' + $location.Substring($home_path.Length) | | } | | $proc = if ($script:PS7) { 'pwsh' } else { 'powershell' } | | | | | try { $host.UI.RawUI.WindowTitle = "$proc $location" } catch { } | | } | | | | | | | | | | | if ($script:IsInteractive -and (Get-Module -Name PSReadLine -ListAvailable -ErrorAction SilentlyContinue)) { | | Import-Module PSReadLine -ErrorAction SilentlyContinue | | | | Set-PSReadLineOption -EditMode Windows | | Set-PSReadLineOption -HistorySearchCursorMovesToEnd | | Set-PSReadLineOption -MaximumHistoryCount 10000 | | Set-PSReadLineOption -HistoryNoDuplicates | | | | | Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward | | Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward | | | | | Set-PSReadLineKeyHandler -Key 'Ctrl+d' -Function DeleteCharOrExit | | | | if ($script:PS7) { | | | try { | | Set-PSReadLineOption -PredictionSource History | | } catch { } | | } | | } | | | | | | | | | | #function Reload-Profile { . $PROFILE } | | #Set-Alias -Name rp -Value Reload-Profile -Force -Scope Global | | | | | function Edit-Profile { | | if (CommandExists 'code') { code $PROFILE } | | elseif (CommandExists 'notepad++') { & 'notepad++' $PROFILE } | | else { notepad $PROFILE } | | } | | #Set-Alias -Name ep -Value Edit-Profile -Force -Scope Global | | | | | | function Enable-GitExtras { $script:GitShowExtras = $true; Write-Host 'Git stash/worktree indicators: ON' -ForegroundColor Green } | | function Disable-GitExtras { $script:GitShowExtras = $false; Write-Host 'Git stash/worktree indicators: OFF' -ForegroundColor DarkGray } | | | | | |


  1. MADRCU?! ↩︎

  2. MADRCU ↩︎

── more in #developer-tools 4 stories · sorted by recency
── more on @powershell 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/powershell-profile-a…] indexed:0 read:26min 2026-07-07 ·