원클릭으로
powershell-config
Conventions for the modular, fast-loading PowerShell profile in this dotfiles repo
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Conventions for the modular, fast-loading PowerShell profile in this dotfiles repo
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Idempotent, self-bootstrapping install scripts for Windows and Unix
Load contract, thin-stub pattern, and module boundaries for the dotfiles repo
How to add a `dotfiles` CLI subcommand with Windows/Unix parity
How to declare packages and register tools across winget/scoop/apt in the dotfiles repo
POSIX-first bash/zsh configuration and Windows parity rules for the dotfiles repo
| name | powershell-config |
| description | Conventions for the modular, fast-loading PowerShell profile in this dotfiles repo |
| domain | powershell |
| confidence | high |
| source | manual |
The PowerShell profile is split into small, focused dot-sourced files under powershell/. The system
$PROFILE contains only a two-line bootstrap stub (written once by bootstrap/install.ps1):
$env:DOTFILES = "<repo-root>"
. "$env:DOTFILES\powershell\profile.ps1"
profile.ps1 is the sole entry point for all real configuration. It is the only file you ever edit
when changing the load order or adding a new top-level concern.
Load order (profile.ps1):
aliases.ps1 — Linux-style wrappers and git shortcutspsreadline.ps1 — PSReadLine options and key bindingsprompt.ps1 — Oh My Posh initialisation with full fallback chaincompletions.ps1 — module imports (Terminal-Icons, posh-git, zoxide, PSFzf) and argument completersmodules/*.ps1 — drop-in files, dot-sourced alphabetically (auto-discovered, no registration)bin/ prepended to $env:PATH (idempotent — checks for existing entry first)All temp variables ($_dot, $_modulesDir, $_binDir, $_vtSupported, $_ompTheme) are cleaned
up with Remove-Variable … -ErrorAction SilentlyContinue at the end of each file so they never
pollute the session.
Alias source of truth: shared/aliases.json. Trinity implements in powershell/aliases.ps1;
Tank implements the same catalog in shell/common.sh. Every alias visible to the user should have
a matching entry in shared/aliases.json before it is implemented in either shell.
Every external tool call is wrapped in a Get-Command X -ErrorAction SilentlyContinue check.
A missing binary must never throw, never warn, and never slow down startup.
if (Get-Command eza -ErrorAction SilentlyContinue) {
function ls { eza --icons --group-directories-first @args }
} else {
function ls { Get-ChildItem @args } # built-in fallback
}
The same guard applies to module imports:
if (Get-Module Terminal-Icons -ListAvailable -ErrorAction SilentlyContinue) {
Import-Module Terminal-Icons -ErrorAction SilentlyContinue
}
And to Invoke-Expression-based initialisers:
if (Get-Command zoxide -ErrorAction SilentlyContinue) {
Invoke-Expression (& { (zoxide init powershell | Out-String) })
}
Use a function, never Set-Alias, whenever the alias must forward arguments or flags. @args
is the splatting idiom that passes all positional and named arguments through transparently.
function grep { rg @args } # forwards -i, -l, --glob, etc.
function cat { bat --style=plain @args }
Use Set-Alias only for zero-argument identity mappings (e.g. Set-Alias vi vim) — but prefer
functions even there if future argument support is plausible.
Theme resolution uses $env:DOTFILES, not any user-specific path. The chain is:
$env:DOTFILES\powershell\themes\dotfiles.omp.json$env:POSH_THEMES_PATH\jandedobbeleer.omp.jsonprompt function (if oh-my-posh is not installed at all)if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) {
$_ompTheme = if ($env:DOTFILES -and (Test-Path "$env:DOTFILES\powershell\themes\dotfiles.omp.json")) {
"$env:DOTFILES\powershell\themes\dotfiles.omp.json"
} elseif ($env:POSH_THEMES_PATH -and (Test-Path "$env:POSH_THEMES_PATH\jandedobbeleer.omp.json")) {
"$env:POSH_THEMES_PATH\jandedobbeleer.omp.json"
} else { $null }
if ($_ompTheme) {
oh-my-posh init pwsh --config $_ompTheme | Invoke-Expression
} else {
oh-my-posh init pwsh | Invoke-Expression
}
Remove-Variable _ompTheme
} else {
function prompt {
$branch = ''
if (Get-Command git -ErrorAction SilentlyContinue) {
$ref = git symbolic-ref --short HEAD 2>$null
if ($ref) { $branch = " [$ref]" }
}
"$($executionContext.SessionState.Path.CurrentLocation)$branch`n> "
}
}
ListView requires a VT-capable console with a known window width. Wrap it in a check so it
degrades to InlineView in non-interactive contexts (test runners, pwsh -NonInteractive, etc.):
if (-not (Get-Module PSReadLine -ErrorAction SilentlyContinue)) { return }
if (-not [System.Environment]::UserInteractive) { return }
$_vtSupported = $Host.UI.RawUI -and $Host.UI.RawUI.WindowSize.Width -gt 0
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
if ($_vtSupported) {
Set-PSReadLineOption -PredictionViewStyle ListView
}
Remove-Variable _vtSupported -ErrorAction SilentlyContinue
To add a self-contained feature (e.g. dotfiles-agent.psm1 or a new helper script), drop a .ps1
file in powershell\modules\. It is automatically dot-sourced in alphabetical order after the
four core files. No registration or changes to profile.ps1 are needed.
# profile.ps1 — modules loop (do not change this)
$_modulesDir = "$_dot\powershell\modules"
if (Test-Path $_modulesDir) {
Get-ChildItem -Path $_modulesDir -Filter '*.ps1' |
Sort-Object Name |
ForEach-Object { . $_.FullName }
}
bin/ PATH prepend (idempotent)$_binDir = "$_dot\bin"
if ((Test-Path $_binDir) -and ($env:PATH -notlike "*$_binDir*")) {
$env:PATH = "$_binDir$([IO.Path]::PathSeparator)$env:PATH"
}
The check $env:PATH -notlike "*$_binDir*" prevents duplicates on reload.
When adding a new alias, add it to shared/aliases.json first, then implement in
powershell\aliases.ps1 (Trinity) and shell\common.sh (Tank).
"myalias": {
"_note": "Human-readable description of what it does",
"windows": "preferred-cmdlet | fallback-cmdlet",
"unix": "preferred-tool | fallback-tool"
}
Omit windows or unix if the alias is platform-specific. Prefix-only metadata keys with _
(e.g. _comment, _owner) — these are skipped by tooling.
# aliases.ps1
if (Get-Command eza -ErrorAction SilentlyContinue) {
function ls { eza --icons --group-directories-first @args }
function ll { eza -la --icons --group-directories-first @args }
function la { eza -a --icons @args }
function l { eza --icons @args }
function lt { eza --tree --level=2 --icons @args }
} else {
function ls { Get-ChildItem @args }
function ll { Get-ChildItem -Force @args }
function la { Get-ChildItem -Force @args }
function l { Get-ChildItem @args }
function lt { Get-ChildItem -Recurse @args }
}
$_modulesDir = "$_dot\powershell\modules"
if (Test-Path $_modulesDir) {
Get-ChildItem -Path $_modulesDir -Filter '*.ps1' |
Sort-Object Name |
ForEach-Object { . $_.FullName }
}
Drop powershell\modules\my-feature.ps1 and it loads automatically — no edits to profile.ps1.
if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) {
$_ompTheme = if ($env:DOTFILES -and (Test-Path "$env:DOTFILES\powershell\themes\dotfiles.omp.json")) {
"$env:DOTFILES\powershell\themes\dotfiles.omp.json"
} elseif ($env:POSH_THEMES_PATH -and (Test-Path "$env:POSH_THEMES_PATH\jandedobbeleer.omp.json")) {
"$env:POSH_THEMES_PATH\jandedobbeleer.omp.json"
} else { $null }
if ($_ompTheme) { oh-my-posh init pwsh --config $_ompTheme | Invoke-Expression }
else { oh-my-posh init pwsh | Invoke-Expression }
Remove-Variable _ompTheme
} else {
function prompt {
$branch = ''
if (Get-Command git -ErrorAction SilentlyContinue) {
$ref = git symbolic-ref --short HEAD 2>$null
if ($ref) { $branch = " [$ref]" }
}
"$($executionContext.SessionState.Path.CurrentLocation)$branch`n> "
}
}
if (-not (Get-Module PSReadLine -ErrorAction SilentlyContinue)) { return }
if (-not [System.Environment]::UserInteractive) { return }
$_vtSupported = $Host.UI.RawUI -and $Host.UI.RawUI.WindowSize.Width -gt 0
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
if ($_vtSupported) { Set-PSReadLineOption -PredictionViewStyle ListView }
if ((Get-Command fzf -ErrorAction SilentlyContinue) -and
(Get-Module PSFzf -ListAvailable -ErrorAction SilentlyContinue)) {
Import-Module PSFzf -ErrorAction SilentlyContinue
Set-PsFzfOption -PSReadlineChordProvider 'Ctrl+t'
Set-PsFzfOption -PSReadlineChordReverseHistory 'Ctrl+r'
}
# BAD — throws on a fresh machine; breaks every startup
oh-my-posh init pwsh --config "$env:DOTFILES\powershell\themes\dotfiles.omp.json" | Invoke-Expression
Always wrap with Get-Command oh-my-posh -ErrorAction SilentlyContinue first.
# BAD — clobbers the built-in and cannot forward arguments
Set-Alias ls eza
Use a function instead so @args forwarding works and the built-in can be reached if needed.
# BAD — breaks on any machine that is not josecorral's
oh-my-posh init pwsh --config "C:\Users\josecorral\poshv3.json" | Invoke-Expression
All paths must be relative to $env:DOTFILES. Never embed a username or absolute drive path.
# BAD — network call during startup; blocks the shell for seconds
$latestRelease = Invoke-RestMethod "https://api.github.com/repos/..."
Profile code must be synchronous and local-only. Anything that touches the network or disk heavily should be a lazy-loaded module or a separate command, not part of the dot-sourced chain.
# BAD — PowerShell-only alias, no catalog entry, Tank never sees it
function gco { git checkout @args }
Add the entry to shared/aliases.json with _note, windows, and unix fields. Then implement
in powershell\aliases.ps1 (Trinity) and shell\common.sh (Tank). Single source of truth prevents
catalog drift between shells.
# BAD — profile.ps1 becomes a dumping ground
# profile.ps1
function my-new-feature { ... }
Add a file to powershell\modules\ instead. profile.ps1 only ever contains the load-order
skeleton and the bin/ PATH prepend — never feature logic.
# BAD — Import-Module prints an error if the module is not installed
Import-Module Terminal-Icons
Always guard with Get-Module Terminal-Icons -ListAvailable -ErrorAction SilentlyContinue first,
then import with -ErrorAction SilentlyContinue.