- Зачем:
- копии списка в профилях разошлись: в PowerShell и VS Code не хватало
::1 и [::1], из-за чего один и тот же хост то шёл мимо прокси, то нет.
- Что:
- profile.ps1 и server-env-setup приведены к выдаче proxy-client.
- добавлен tests/test_proxy_env_sync.py — сверяет каждую копию с эталоном
DEFAULT_NO_PROXY_EXTRA и списки обхода Windows между собой.
- в файлах-копиях и README указан эталон и тест, который ловит расхождение.
- Проверка:
- python3 tests/test_proxy_env_sync.py (2 OK, на подложенном расхождении падает).
- python3 tests/test_proxy_client.py (54 OK), bash tests/test_proxy_launchers.sh (8 PASS).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
207 lines
8.9 KiB
PowerShell
207 lines
8.9 KiB
PowerShell
# ── 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 }
|