# ── UTF-8 ──────────────────────────────────────────────────────────── # Принудительный UTF-8 для pwsh 7 в интерактивной консоли, # независимо от унаследованной code page. # НЕ трогает PS 5.1, VS Code terminal, remoting и т.д. if ($IsWindows -and $PSVersionTable.PSEdition -eq 'Core' -and $Host.Name -eq 'ConsoleHost') { chcp 65001 > $null $Utf8 = New-Object System.Text.UTF8Encoding($false) [Console]::InputEncoding = $Utf8 [Console]::OutputEncoding = $Utf8 $OutputEncoding = $Utf8 } # ── PSReadLine ─────────────────────────────────────────────────────── # Предиктивный ввод по истории, только для интерактивной консоли. if ( $Host.Name -eq 'ConsoleHost' -and -not [Console]::IsOutputRedirected -and $Host.UI.SupportsVirtualTerminal -and (Get-Module -ListAvailable -Name PSReadLine) ) { Import-Module PSReadLine -ErrorAction SilentlyContinue Set-PSReadLineOption -PredictionSource History -ErrorAction SilentlyContinue Set-PSReadLineOption -PredictionViewStyle ListView -ErrorAction SilentlyContinue Set-PSReadLineOption -HistoryNoDuplicates:$true -ErrorAction SilentlyContinue } # ── Unix-хелперы (lowercase — привычка из bash) ───────────────────── # Перечитать профиль без перезапуска сессии. function Reload-Profile { . $PROFILE } # Показать все команды с данным именем (аналог which в bash). function which ($Name) { Get-Command -Name $Name -All | Select-Object CommandType, Name, Source } # Создать файл или обновить mtime (аналог touch в bash). function touch { param([Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$Path) foreach ($Item in $Path) { if (Test-Path -LiteralPath $Item) { (Get-Item -LiteralPath $Item).LastWriteTime = Get-Date } else { New-Item -ItemType File -Path $Item -Force | Out-Null } } } # mkdir + cd в одно действие. function mkcd ($Path) { New-Item -ItemType Directory -Path $Path -Force | Out-Null Set-Location -Path $Path } # Открыть путь в Explorer (аналог xdg-open / macOS open). function open ($Path = '.') { Invoke-Item -Path $Path } # Перейти в корень текущего git-репозитория. function croot { if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Error 'git is not available in PATH.'; return } $Root = git rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($Root)) { Write-Error 'Not inside a Git repository.'; return } Set-Location -Path $Root.Trim() } # ── Прокси (аналог bash proxy_on/proxy_off/myip) ──────────────────── # Получить текущий внешний IP (или $null при ошибке/таймауте). function Get-PublicIp { try { return ([string](Invoke-RestMethod -Uri 'https://ifconfig.me/ip' -TimeoutSec 3)).Trim() } catch { return $null } } # Единый источник правды для прокси-эндпоинта в PowerShell. # Список NoProxy обязан совпадать с тем, что выдаёт proxy-client # (см. DEFAULT_NO_PROXY_EXTRA в proxy-client/.local/bin/proxy-client); # расхождение ловит tests/test_proxy_env_sync.py. function Get-ProxyEndpoint { @{ ProxyHost = 'hproxy.local' Port = 12334 NoProxy = 'hproxy.local,localhost,127.0.0.1,::1,[::1],.concept-software.ru,dementev.space,.dementev.space' } } # Внутренний: выставить proxy-env в текущей области. Единственное место, # где формируются значения переменных. function Set-ProxyEnv { $e = Get-ProxyEndpoint $env:HTTP_PROXY = "http://$($e.ProxyHost):$($e.Port)" $env:HTTPS_PROXY = "http://$($e.ProxyHost):$($e.Port)" $env:ALL_PROXY = "socks5://$($e.ProxyHost):$($e.Port)" $env:NO_PROXY = $e.NoProxy } # Внутренний: fail-fast проверка доступности прокси (только reachability). # Таймаут — $env:PROXY_PREFLIGHT_TIMEOUT (дефолт 2 c), покрывает и медленный DNS. function Test-ProxyReachable { $e = Get-ProxyEndpoint $timeout = if ($env:PROXY_PREFLIGHT_TIMEOUT) { [int]$env:PROXY_PREFLIGHT_TIMEOUT } else { 2 } $client = [System.Net.Sockets.TcpClient]::new() try { $iar = $client.BeginConnect($e.ProxyHost, [int]$e.Port, $null, $null) if ($iar.AsyncWaitHandle.WaitOne([TimeSpan]::FromSeconds($timeout))) { $client.EndConnect($iar) return $true } return $false } catch { return $false } finally { $client.Dispose() } } # Включить прокси для текущей сессии (hproxy.local). function proxy_on { Set-ProxyEnv $Ip = Get-PublicIp "Proxy ON. IP: $(if ($Ip) { $Ip } else { 'unavailable' })" } # Выключить прокси для текущей сессии. function proxy_off { foreach ($V in 'HTTP_PROXY','HTTPS_PROXY','ALL_PROXY','NO_PROXY') { Remove-Item -Path "Env:$V" -ErrorAction SilentlyContinue } $Ip = Get-PublicIp "Proxy OFF. IP: $(if ($Ip) { $Ip } else { 'unavailable' })" } # Показать текущий внешний IP. function myip { $Ip = Get-PublicIp "IP: $(if ($Ip) { $Ip } else { 'unavailable' })" } # Внутренний: запуск команды через прокси. Pre-flight, затем env только на # время дочернего процесса (в finally восстанавливаем прежние значения — # сессия не остаётся с включённым прокси). function Invoke-WithProxy { param( [Parameter(Mandatory)][string]$Exe, [Parameter(ValueFromRemainingArguments)][string[]]$CmdArgs ) if (-not (Get-Command $Exe -ErrorAction SilentlyContinue)) { Write-Error "Команда не найдена в PATH: $Exe" return } if (-not (Test-ProxyReachable)) { $e = Get-ProxyEndpoint Write-Error "Прокси $($e.ProxyHost):$($e.Port) недоступен — запуск '$Exe' отменён." return } $names = 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY' $saved = @{} foreach ($n in $names) { $saved[$n] = [Environment]::GetEnvironmentVariable($n) } try { Set-ProxyEnv & $Exe @CmdArgs $global:LASTEXITCODE = $LASTEXITCODE } finally { foreach ($n in $names) { if ($null -eq $saved[$n]) { Remove-Item -Path "Env:$n" -ErrorAction SilentlyContinue } else { Set-Item -Path "Env:$n" -Value $saved[$n] } } } } # Явный запуск claude/codex гарантированно через прокси. function pclaude { Invoke-WithProxy claude @args } function pcodex { Invoke-WithProxy codex @args } # ── Шорткаты Windows Terminal ──────────────────────────────────────── if (Get-Command wt -ErrorAction SilentlyContinue) { # Новое окно / вкладка / split-pane Windows Terminal в текущей директории. function wt_here { wt -d (Get-Location).ProviderPath } function wt_tab_here { wt -w 0 new-tab -d (Get-Location).ProviderPath } function wt_split_here { wt -w 0 split-pane -d (Get-Location).ProviderPath } } # ── Локальные дополнения ───────────────────────────────────────────── # Машинно-специфичные добавки: profile.extra.ps1 или profile.d/*.ps1 $ExtraProfile = Join-Path $HOME '.config\powershell\profile.extra.ps1' if (Test-Path -LiteralPath $ExtraProfile) { . $ExtraProfile } $ExtraProfileDir = Join-Path $HOME '.config\powershell\profile.d' if (Test-Path -LiteralPath $ExtraProfileDir) { Get-ChildItem -Path $ExtraProfileDir -Filter '*.ps1' -File | Sort-Object Name | ForEach-Object { . $_.FullName } } # ── Chocolatey tab-completion ──────────────────────────────────────── $ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" if (Test-Path -LiteralPath $ChocolateyProfile) { Import-Module $ChocolateyProfile }