feat(proxy-client): добавлена самостоятельная установка

- Зачем:
  - пакет должен устанавливаться безопасно без полного dotfiles и обязательного Stow.
- Что:
  - в master влита feature-ветка с установщиком, тестами и цельной инструкцией.
  - история проектирования сохранена, а рабочая зона .scratch удалена перед слиянием.
- Проверка:
  - bash tests/test_proxy_client_install.sh.
  - bash tests/test_install.sh.
This commit is contained in:
ddadmin committed 2026-07-21 22:48:02 +03:00
commit ea5c7afd4c
7 files changed
+822 -53

No files matched your search

View File
Whitespace-only changes.
+102 -12
View File
@@ -2,7 +2,8 @@
Дата: 2026-07-15
Статус: реализация готова к ручной приёмке; публичное имя уточнено 2026-07-21
Статус: основная реализация принята вручную; самостоятельный установщик
спроектирован 2026-07-21, но ещё не реализован
## Проблема
@@ -48,8 +49,9 @@ proxy-переменных совместима с локальным Xray бе
JSON Xray.
- Сохранить совместимость Bash-функций с внешним Hiddify.
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
- Поддержать выборочную установку после `git clone`: через stow, ручные
симлинки или копирование.
- Поддержать самостоятельную установку из отдельно скопированного каталога
`proxy-client/` без зависимости от общего установщика dotfiles и без
обязательного stow.
## Не входит в объём
@@ -89,6 +91,22 @@ Bash нужен только там, где необходимо изменит
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
`pcodex` из точечной ссылки Hiddify.
### Доставка отдельного пакета
Рассмотрены четыре способа подключить отдельно скопированный каталог:
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
легко забываемые шаги;
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
но усложняет обновления и расходится с моделью dotfiles;
- требовать stow — даёт привычное управление ссылками, но добавляет
необязательную зависимость на удалённых серверах;
- дать пакету собственный установщик, который использует stow при наличии и
создаёт те же симлинки напрямую без него.
Выбран последний вариант. Он сохраняет один результат установки при разном
составе машины и не требует второй копии обновляемых файлов.
## Рекомендуемое решение
### Границы компонентов
@@ -376,15 +394,50 @@ CLI раскладывается как `~/.local/bin/proxy-client`. Машин
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
файлы.
В корне этого каталога находится самостоятельный `README.md`. Он является
основной инструкцией клиента и исключается из раскладки stow. README остаётся
рядом с кодом при выборочном клонировании или копировании каталога. Корневой
README репозитория только кратко описывает подсистему и ссылается на него.
В корне этого каталога находятся самостоятельный `README.md` и пакетный
`install.sh`. Оба файла относятся к исходному каталогу и исключаются из
раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
копировании каталога. Корневой README репозитория только кратко описывает
подсистему и ссылается на него.
После клонирования репозитория пакет можно подключить отдельно от общего
`install.sh`. README также должен описывать эквивалентные ручные симлинки и
копирование файлов. Полный `install.sh` подключает этот же stow-пакет вместе с
остальными dotfiles и не имеет отдельной реализации.
Пакетный установщик связывает отдельно скопированный каталог с домашним
каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
доступен, установщик использует его; если нет — создаёт тот же набор симлинков
напрямую. Выбранный механизм явно показывается пользователю, но не меняет
результат. Исходный каталог должен оставаться на прежнем пути всё время
использования ссылок.
Установщик управляет только тремя публичными артефактами: CLI, `shell.bash` и
каталогом шаблонов. Машинный `config` и закрытый `default.url` не входят в
раскладку stow, никогда не создаются, не заменяются и не связываются
установщиком. Их наличие в исходном каталоге также не должно расширять набор
управляемых путей.
Домашние `~/.local/bin` и `~/.config/proxy-client` остаются настоящими
каталогами. Stow не должен сворачивать их или их родителей в одну ссылку на
исходное дерево: ссылками становятся только три перечисленных артефакта. Это
одновременно сохраняет место для машинных файлов и делает результат stow
совпадающим с прямой раскладкой через `ln -s`.
Перед записью установщик проверяет весь набор назначений. Правильная ссылка на
текущий исходный каталог считается уже установленной; любой другой файл,
каталог или симлинк считается конфликтом. При конфликте установщик ничего не
меняет и сообщает все пути, которые пользователь должен разобрать вручную.
Принудительной замены и автоматических резервных копий нет.
По умолчанию установщик идемпотентно подключает Bash-модуль в `~/.bashrc` и при
необходимости добавляет `~/.local/bin` в `PATH`. Уже существующее подключение
через dotfiles не дублируется. Управляемый блок помечается, повторно не
добавляется и может быть полностью отключён параметром `--no-shell-setup`.
Установщик не выбирает режим прокси, не создаёт конфигурацию, не читает VLESS,
не устанавливает Xray и не проверяет сеть. После раскладки он проверяет только
локальные требования и исправность CLI и Bash-модуля, затем печатает краткую
подсказку для текущей shell-сессии и направляет пользователя в README за
настройкой режима. Недоступный прокси не делает установку неуспешной.
Полный корневой `install.sh` продолжает подключать тот же stow-пакет вместе с
остальными dotfiles и не получает отдельной реализации этой логики.
## Решения и отклонённые альтернативы
@@ -412,6 +465,14 @@ README репозитория только кратко описывает по
автоматическое распределение портов отложены до появления реальной нужды.
- Официальный системный установщик отклонён: он требует root и создаёт
systemd-службы. Пользовательская установка соответствует выбранной модели.
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
усложняет обновление и нарушает общую с dotfiles модель симлинков.
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
результат на машине без stow, а автоматический выбор механизма не меняет
публичное поведение пакета.
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
установщика пакета. Эти операции зависят от машины, могут требовать секретов
или сети и уже имеют отдельные явные команды и инструкции.
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
@@ -428,6 +489,15 @@ README репозитория только кратко описывает по
истинное значение сопровождается предупреждением.
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
основной работы и сообщает обнаруженную и требуемую версии.
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
сообщает эту зависимость и при повторном запуске принимает только ссылки на
текущий источник.
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
оболочки. Установщик распознаёт собственный блок и известное подключение из
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
- Частичная установка может оставить неоднозначное состояние. Все конфликты
назначений проверяются до изменений; проверки охватывают отсутствие новых
ссылок и правок shell при отказе.
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
и домены, а CIDR описывает как совместимость best-effort.
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
@@ -463,6 +533,22 @@ README репозитория только кратко описывает по
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`.
- Проверки самостоятельной установки покрывают путь со stow и без него,
одинаковый набор итоговых ссылок, запуск не из каталога пакета и каталог с
пробелами в пути.
- В обоих путях домашние каталоги остаются обычными каталогами, а пакетные
`README.md` и `install.sh` не появляются в домашнем каталоге через stow.
- Повторный запуск не меняет уже правильную установку. Конфликт любого
назначения обнаруживается до записи; чужие файлы, каталоги и ссылки остаются
неизменными.
- Машинный `config` и закрытый `default.url` не управляются установщиком даже
при их наличии в исходном каталоге или домашнем каталоге.
- Настройка Bash проверяется для нового `~/.bashrc`, уже помеченного блока,
подключения из полного dotfiles и режима `--no-shell-setup`. Добавление
`~/.local/bin` не дублируется.
- Локальная проверка установки не обращается к сети и не требует доступного
прокси. Неисправный или отсутствующий машинный конфиг не отменяет успешную
раскладку пакета.
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
доступности: недоступная среда явно записывается как неподтверждённая, а
@@ -474,7 +560,11 @@ README репозитория только кратко описывает по
реализацией. Она должна описывать:
- различие внешнего Hiddify и управляемого Xray;
- выборочная установка через stow, ручные симлинки и копирование;
- самостоятельный `install.sh`, его автоматический выбор stow или прямых
симлинков, требование сохранять исходный каталог и режим
`--no-shell-setup`;
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
конфигом и закрытым профилем;
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
+3
View File
@@ -1 +1,4 @@
^/README\.md$
^/install\.sh$
^/\.config/proxy-client/config$
^/\.config/proxy-client/default\.url$
+98 -41
View File
@@ -10,41 +10,68 @@ CLI требует Python 3.8+ и использует только станда
## Установка
### Только пакет `proxy-client`
Скопируйте каталог в постоянное место и запустите пакетный установщик. Текущий
рабочий каталог не важен:
```bash
~/src/proxy-client/install.sh
```
При наличии Stow установщик использует его, иначе создаёт те же три симлинка
напрямую: CLI, Bash-модуль и каталог шаблонов. Исходный каталог после установки
нельзя перемещать: домашние ссылки зависят от его постоянного пути.
По умолчанию установщик добавляет в `~/.bashrc` помеченный блок для
`~/.local/bin` и Bash-модуля. После установки откройте новый Bash или примените
блок в текущей сессии:
```bash
source ~/.bashrc
```
Чтобы оставить `~/.bashrc` без изменений, запустите:
```bash
~/src/proxy-client/install.sh --no-shell-setup
```
В этом случае подготовьте только текущую сессию вручную:
```bash
export PATH="$HOME/.local/bin:$PATH"
source ~/.config/proxy-client/shell.bash
```
Для следующих сессий добавьте эти строки в подходящий Bash-конфиг сами.
Установщик не создаёт машинный `config` и закрытый `default.url`, не
устанавливает Xray и не проверяет сеть. После подключения команд выберите режим
по разделу [«Настройка режима»](#настройка-режима). Без машинного конфига
действует режим Hiddify по умолчанию.
### В составе dotfiles
Полная установка подключает пакет и автоматически загружает Bash-функции в
новых интерактивных сессиях:
Корневой установщик dotfiles подключает тот же пакет через Stow и загружает
Bash-функции в новых интерактивных сессиях:
```bash
cd ~/dotfiles
./install.sh
```
Установщик заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow
Он заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow
размещал ссылки внутри них, а не превращал сами каталоги в ссылки на
репозиторий.
После установки откройте новый Bash или выполните:
репозиторий. После установки откройте новый Bash или выполните:
```bash
export PATH="$HOME/.local/bin:$PATH"
source ~/.bash_custom
```
### Только пакет `proxy-client`
```bash
cd ~/dotfiles
mkdir -p ~/.local/bin ~/.config/proxy-client
stow proxy-client
source ~/.config/proxy-client/shell.bash
```
Если Bash-пакет dotfiles не установлен, добавьте подключение модуля в
`~/.bashrc`:
```bash
printf '%s\n' 'source ~/.config/proxy-client/shell.bash' >> ~/.bashrc
```
Если `~/.local/bin` отсутствует в `PATH` и в новых сессиях, добавьте ту же строку
`export` в свой Bash-конфиг: корневой установщик dotfiles его не изменяет.
## Как выбирается прокси
@@ -80,28 +107,46 @@ proxy-client status
pclaude --version
```
В WSL NAT mode IP Windows-шлюза меняется при перезапуске. Интерактивный
`./install.sh` предлагает добавить обновление `hproxy.local` в `/etc/wsl.conf`.
То же можно настроить вручную:
#### WSL NAT mode
```ini
[boot]
command = "/path/to/dotfiles/scripts/setup-hproxy.sh"
IP Windows-шлюза меняется при перезапуске WSL, поэтому `hproxy.local` нужно
обновлять. В отдельно скопированном пакете системных помощников нет. Узнайте
адрес шлюза:
```bash
ip route show default | awk 'NR == 1 {print $3}'
```
После изменения выполните `wsl --shutdown` из Windows. Обновить адрес без
перезапуска WSL можно так:
Затем откройте `/etc/hosts` через `sudoedit` и оставьте одну отдельную строку с
полученным адресом:
```bash
sudoedit /etc/hosts
```
```text
<адрес-шлюза> hproxy.local
```
Не удаляйте другие имена на той же строке: при необходимости перенесите их на
отдельную строку. Повторяйте обновление после перезапуска WSL.
С полным dotfiles запустите корневой `~/dotfiles/install.sh` интерактивно: он
предложит автоматическое обновление при старте WSL. Обновить адрес сразу можно
так:
```bash
sudo ~/dotfiles/scripts/setup-hproxy.sh
# или после загрузки Bash-функций:
# после настройки автозапуска и загрузки общих Bash-функций:
update_hproxy
```
В NAT mode Hiddify должен слушать адрес, доступный через Windows-шлюз, а не
только `127.0.0.1`.
В [WSL mirrored mode](https://learn.microsoft.com/windows/wsl/networking#mirrored-mode-networking)
#### WSL mirrored mode
В [mirrored mode](https://learn.microsoft.com/windows/wsl/networking#mirrored-mode-networking)
(`networkingMode=mirrored`) Windows-сервисы доступны из WSL через `127.0.0.1`.
Чтобы сохранить адрес по умолчанию, добавьте алиас в Windows hosts:
@@ -110,9 +155,9 @@ update_hproxy
127.0.0.1 hproxy.local
```
Альтернатива — создать `config.external` с `address = 127.0.0.1`. В обоих
случаях проверьте результат через `getent hosts hproxy.local` или
`proxy-client status`.
Альтернатива — настройте внешний режим по следующему разделу и укажите
`address = 127.0.0.1`. Алиас проверяйте через `getent hosts hproxy.local`, а
активный конфиг — через `proxy-client status`.
### Другой внешний прокси
@@ -158,7 +203,7 @@ chmod 600 ~/.config/proxy-client/default.url
proxy-client install
```
Запустите Xray в отдельной панели tmux:
Запустите Xray в отдельном терминале или панели tmux:
```bash
proxy-client start
@@ -247,20 +292,32 @@ export PROXY_PREFLIGHT_TIMEOUT=5
## Справочник
### Ручное подключение без Stow
### Ручное подключение без установщика
Можно создать ссылки на CLI, Bash-модуль и примеры либо скопировать те же файлы:
Если пакетный установщик запустить нельзя, выберите постоянный абсолютный путь
к исходному каталогу и создайте те же ссылки вручную. Команды предполагают, что
назначения ещё не заняты:
```bash
mkdir -p ~/.local/bin ~/.config/proxy-client
ln -s /path/to/proxy-client/.local/bin/proxy-client ~/.local/bin/proxy-client
ln -s /path/to/proxy-client/.config/proxy-client/shell.bash \
ln -s /absolute/path/to/proxy-client/.local/bin/proxy-client \
~/.local/bin/proxy-client
ln -s /absolute/path/to/proxy-client/.config/proxy-client/shell.bash \
~/.config/proxy-client/shell.bash
ln -s /path/to/proxy-client/.config/proxy-client/examples \
ln -s /absolute/path/to/proxy-client/.config/proxy-client/examples \
~/.config/proxy-client/examples
chmod 755 /absolute/path/to/proxy-client/.local/bin/proxy-client
```
Исполняемому файлу нужен режим `755`, а `~/.local/bin` должен входить в `PATH`.
Подготовьте текущую Bash-сессию:
```bash
export PATH="$HOME/.local/bin:$PATH"
source ~/.config/proxy-client/shell.bash
```
Для следующих сессий добавьте эти две строки в свой Bash-конфиг. Исходный
каталог после этого нельзя перемещать: созданные ссылки зависят от его пути.
### Машинный конфиг
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
set -euo pipefail
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
STOW_BIN="${STOW_BIN:-stow}"
SETUP_SHELL=1
usage() {
cat <<'EOF'
Использование: ./install.sh [--no-shell-setup]
--no-shell-setup не изменять ~/.bashrc
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--no-shell-setup)
SETUP_SHELL=0
;;
-h|--help)
usage
exit 0
;;
*)
echo "Ошибка: неизвестный параметр: $1" >&2
usage >&2
exit 2
;;
esac
shift
done
SOURCES=(
"$PACKAGE_DIR/.local/bin/proxy-client"
"$PACKAGE_DIR/.config/proxy-client/shell.bash"
"$PACKAGE_DIR/.config/proxy-client/examples"
)
TARGETS=(
"$HOME/.local/bin/proxy-client"
"$HOME/.config/proxy-client/shell.bash"
"$HOME/.config/proxy-client/examples"
)
STOW_IGNORE_PATTERNS=(
'^\.local/bin/proxy-client$'
'^\.config/proxy-client/shell\.bash$'
'^\.config/proxy-client/examples$'
)
source_errors=()
[ -x "${SOURCES[0]}" ] || source_errors+=("${SOURCES[0]}")
[ -f "${SOURCES[1]}" ] || source_errors+=("${SOURCES[1]}")
[ -d "${SOURCES[2]}" ] || source_errors+=("${SOURCES[2]}")
if [ "${#source_errors[@]}" -gt 0 ]; then
echo "Ошибка: отсутствуют обязательные файлы исходного пакета:" >&2
printf ' %s\n' "${source_errors[@]}" >&2
exit 1
fi
"${SOURCES[0]}" --help >/dev/null
bash -n "${SOURCES[1]}"
SHELL_BLOCK_START='# >>> proxy-client >>>'
SHELL_BLOCK_END='# <<< proxy-client <<<'
SHELL_SETUP_NEEDED=0
BASH_CUSTOM_HAS_MODULE=0
SHELL_PATH_NEEDED=1
SHELL_SOURCE_NEEDED=1
SHELL_BLOCK_CONFLICT=0
if [ "$SETUP_SHELL" -eq 1 ]; then
if [ -f "$HOME/.bash_custom" ] \
&& grep -Fq 'proxy-client/shell.bash' "$HOME/.bash_custom"; then
BASH_CUSTOM_HAS_MODULE=1
SHELL_SOURCE_NEEDED=0
fi
bashrc_start_count=0
bashrc_end_count=0
if [ -f "$HOME/.bashrc" ]; then
bashrc_start_count="$(
grep -Fxc "$SHELL_BLOCK_START" "$HOME/.bashrc" || true
)"
bashrc_end_count="$(
grep -Fxc "$SHELL_BLOCK_END" "$HOME/.bashrc" || true
)"
if grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
SHELL_PATH_NEEDED=0
fi
fi
if [ "$bashrc_start_count" -eq 0 ] && [ "$bashrc_end_count" -eq 0 ]; then
if [ "$SHELL_PATH_NEEDED" -eq 1 ] \
|| [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
SHELL_SETUP_NEEDED=1
fi
elif [ "$bashrc_start_count" -eq 1 ] \
&& [ "$bashrc_end_count" -eq 1 ] \
&& [ "$SHELL_PATH_NEEDED" -eq 0 ] \
&& { [ "$BASH_CUSTOM_HAS_MODULE" -eq 1 ] \
|| grep -Fq 'proxy-client/shell.bash' "$HOME/.bashrc"; }; then
:
else
SHELL_BLOCK_CONFLICT=1
fi
fi
conflicts=()
missing_targets=0
stow_ignore_args=()
PARENT_DIRS=(
"$HOME/.local"
"$HOME/.local/bin"
"$HOME/.config"
"$HOME/.config/proxy-client"
)
for parent_dir in "${PARENT_DIRS[@]}"; do
if { [ -e "$parent_dir" ] || [ -L "$parent_dir" ]; } \
&& { [ ! -d "$parent_dir" ] || [ -L "$parent_dir" ]; }; then
conflicts+=("$parent_dir")
fi
done
for index in "${!TARGETS[@]}"; do
target="${TARGETS[$index]}"
source_path="${SOURCES[$index]}"
if [ -e "$target" ] || [ -L "$target" ]; then
if [ ! -L "$target" ] \
|| [ "$(readlink -f "$target" 2>/dev/null || true)" != "$source_path" ]; then
conflicts+=("$target")
else
stow_ignore_args+=("--ignore=${STOW_IGNORE_PATTERNS[$index]}")
fi
else
missing_targets=$((missing_targets + 1))
fi
done
if [ "$SHELL_SETUP_NEEDED" -eq 1 ] \
&& { [ -e "$HOME/.bashrc" ] || [ -L "$HOME/.bashrc" ]; } \
&& [ ! -f "$HOME/.bashrc" ]; then
conflicts+=("$HOME/.bashrc")
fi
if [ "$SHELL_BLOCK_CONFLICT" -eq 1 ]; then
conflicts+=("$HOME/.bashrc")
fi
if [ "${#conflicts[@]}" -gt 0 ]; then
echo "Ошибка: найдены конфликтующие пути:" >&2
printf ' %s\n' "${conflicts[@]}" >&2
echo "Разберите их вручную и повторите установку." >&2
exit 1
fi
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
if command -v "$STOW_BIN" >/dev/null 2>&1; then
echo "Установка с помощью Stow."
if [ "$missing_targets" -gt 0 ]; then
"$STOW_BIN" --dir="$(dirname "$PACKAGE_DIR")" --target="$HOME" \
"${stow_ignore_args[@]}" \
"$(basename "$PACKAGE_DIR")"
fi
else
echo "Установка без Stow: создаю символические ссылки напрямую."
for index in "${!TARGETS[@]}"; do
target="${TARGETS[$index]}"
source_path="${SOURCES[$index]}"
if [ ! -L "$target" ]; then
ln -s "$source_path" "$target"
fi
done
fi
"$HOME/.local/bin/proxy-client" --help >/dev/null
bash -n "$HOME/.config/proxy-client/shell.bash"
if [ "$SHELL_SETUP_NEEDED" -eq 1 ]; then
cat >> "$HOME/.bashrc" <<'EOF'
# >>> proxy-client >>>
EOF
if [ "$SHELL_PATH_NEEDED" -eq 1 ]; then
cat >> "$HOME/.bashrc" <<'EOF'
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*)
export PATH="$HOME/.local/bin:$PATH"
;;
esac
EOF
fi
if [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
cat >> "$HOME/.bashrc" <<'EOF'
[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"
EOF
fi
cat >> "$HOME/.bashrc" <<'EOF'
# <<< proxy-client <<<
EOF
elif [ "$SETUP_SHELL" -eq 0 ]; then
echo "Настройка Bash пропущена (--no-shell-setup)."
fi
if [ "$SETUP_SHELL" -eq 1 ]; then
RELOAD_COMMAND='source ~/.bashrc'
else
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
fi
cat <<EOF
proxy-client установлен.
Исходный каталог должен оставаться по постоянному пути:
$PACKAGE_DIR
Примените настройки в текущей сессии: $RELOAD_COMMAND
Настройка режима описана в $PACKAGE_DIR/README.md.
EOF
+1
View File
@@ -13,5 +13,6 @@ export PYTHONDONTWRITEBYTECODE=1
python3 -m unittest discover -s tests
bash tests/test_proxy_launchers.sh
bash tests/test_proxy_client_install.sh
bash tests/test_install.sh
uv run --python 3.8 --no-project python -m unittest discover -s tests
+402
View File
@@ -0,0 +1,402 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEST_TMP_ROOT="$(mktemp -d /tmp/proxy-client-install-test.XXXXXX)"
trap 'rm -rf "$TEST_TMP_ROOT"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
assert_link_points_to() {
local link_path="$1" expected_path="$2" actual_path
[ -L "$link_path" ] || fail "Expected symlink: $link_path"
actual_path="$(readlink -f "$link_path")"
[ "$actual_path" = "$expected_path" ] \
|| fail "$link_path points to $actual_path, expected $expected_path"
}
assert_not_exists() {
local path="$1"
[ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path"
}
setup_package() {
local fixture_root="$1" package_dir="$fixture_root/source with spaces/proxy-client"
mkdir -p \
"$package_dir/.local/bin" \
"$package_dir/.config/proxy-client"
cp -a \
"$REPO_ROOT/proxy-client/.local/bin/proxy-client" \
"$package_dir/.local/bin/proxy-client"
cp -a \
"$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash" \
"$REPO_ROOT/proxy-client/.config/proxy-client/examples" \
"$package_dir/.config/proxy-client/"
cp -a \
"$REPO_ROOT/proxy-client/.stow-local-ignore" \
"$REPO_ROOT/proxy-client/README.md" \
"$REPO_ROOT/proxy-client/install.sh" \
"$package_dir/"
printf '%s\n' 'MACHINE_CONFIG_SENTINEL' > "$package_dir/.config/proxy-client/config"
printf '%s\n' 'PRIVATE_PROFILE_SENTINEL' > "$package_dir/.config/proxy-client/default.url"
printf '%s\n' "$package_dir"
}
run_package_install() {
local package_dir="$1" home_dir="$2" stow_bin="$3"
shift 3
(
cd "$TEST_TMP_ROOT"
HOME="$home_dir" STOW_BIN="$stow_bin" \
"$package_dir/install.sh" "$@"
)
}
test_installs_same_public_artifacts_without_stow() {
local fixture_root package_dir home_dir output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
output="$(
run_package_install "$package_dir" "$home_dir" missing-stow --no-shell-setup
)"
printf '%s' "$output" | grep -Fq 'без Stow' \
|| fail "Expected direct-symlink method in output"
printf '%s' "$output" | grep -Fq \
'export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash' \
|| fail "Expected complete current-session hint with --no-shell-setup"
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|| fail "Expected ~/.local/bin to be a real directory"
[ -d "$home_dir/.config/proxy-client" ] \
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|| fail "Expected proxy-client config home to be a real directory"
assert_link_points_to \
"$home_dir/.local/bin/proxy-client" \
"$package_dir/.local/bin/proxy-client"
assert_link_points_to \
"$home_dir/.config/proxy-client/shell.bash" \
"$package_dir/.config/proxy-client/shell.bash"
assert_link_points_to \
"$home_dir/.config/proxy-client/examples" \
"$package_dir/.config/proxy-client/examples"
assert_not_exists "$home_dir/.config/proxy-client/config"
assert_not_exists "$home_dir/.config/proxy-client/default.url"
assert_not_exists "$home_dir/README.md"
assert_not_exists "$home_dir/install.sh"
assert_not_exists "$home_dir/.bashrc"
}
test_configures_bash_once_by_default() {
local fixture_root package_dir home_dir output marker_count path_count source_count
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
output="$(run_package_install "$package_dir" "$home_dir" missing-stow)"
marker_count="$(grep -Fxc '# >>> proxy-client >>>' "$home_dir/.bashrc")"
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
source_count="$(
grep -Fc \
'[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"' \
"$home_dir/.bashrc"
)"
[ "$marker_count" -eq 1 ] || fail "Expected one managed Bash block"
[ "$path_count" -eq 1 ] || fail "Expected one ~/.local/bin PATH line"
[ "$source_count" -eq 1 ] || fail "Expected one shell module source line"
printf '%s' "$output" | grep -Fq 'source ~/.bashrc' \
|| fail "Expected current-session reload hint"
}
test_installs_same_public_artifacts_with_stow() {
local fixture_root package_dir home_dir output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
output="$(run_package_install "$package_dir" "$home_dir" stow --no-shell-setup)"
printf '%s' "$output" | grep -Fq 'с помощью Stow' \
|| fail "Expected Stow method in output"
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|| fail "Expected ~/.local/bin to be a real directory"
[ -d "$home_dir/.config/proxy-client" ] \
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|| fail "Expected proxy-client config home to be a real directory"
assert_link_points_to \
"$home_dir/.local/bin/proxy-client" \
"$package_dir/.local/bin/proxy-client"
assert_link_points_to \
"$home_dir/.config/proxy-client/shell.bash" \
"$package_dir/.config/proxy-client/shell.bash"
assert_link_points_to \
"$home_dir/.config/proxy-client/examples" \
"$package_dir/.config/proxy-client/examples"
assert_not_exists "$home_dir/.config/proxy-client/config"
assert_not_exists "$home_dir/.config/proxy-client/default.url"
assert_not_exists "$home_dir/README.md"
assert_not_exists "$home_dir/install.sh"
}
test_rejects_linked_parent_directories_before_changes() {
local fixture_root package_dir home_dir external_local external_config output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
external_local="$fixture_root/external-local"
external_config="$fixture_root/external-config"
mkdir -p "$home_dir" "$external_local" "$external_config"
ln -s "$external_local" "$home_dir/.local"
ln -s "$external_config" "$home_dir/.config"
if output="$(
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
)"; then
fail "Expected linked parent directories to be rejected"
fi
printf '%s' "$output" | grep -Fq "$home_dir/.local" \
|| fail "Expected ~/.local conflict in output"
printf '%s' "$output" | grep -Fq "$home_dir/.config" \
|| fail "Expected ~/.config conflict in output"
assert_not_exists "$external_local/bin"
assert_not_exists "$external_config/proxy-client"
assert_not_exists "$home_dir/.bashrc"
}
test_reports_all_target_conflicts_without_partial_install() {
local fixture_root package_dir home_dir other_target output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
other_target="$fixture_root/other-target"
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client/shell.bash"
printf '%s\n' 'KEEP_CLI' > "$home_dir/.local/bin/proxy-client"
printf '%s\n' 'KEEP_TARGET' > "$other_target"
ln -s "$other_target" "$home_dir/.config/proxy-client/examples"
if output="$(
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
)"; then
fail "Expected all target conflicts to be rejected"
fi
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|| fail "Expected CLI conflict in output"
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/shell.bash" \
|| fail "Expected shell module conflict in output"
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|| fail "Expected examples conflict in output"
grep -Fq 'KEEP_CLI' "$home_dir/.local/bin/proxy-client" \
|| fail "Expected conflicting CLI file to stay unchanged"
[ -d "$home_dir/.config/proxy-client/shell.bash" ] \
|| fail "Expected conflicting shell directory to stay unchanged"
assert_link_points_to "$home_dir/.config/proxy-client/examples" "$other_target"
assert_not_exists "$home_dir/.bashrc"
}
test_rejects_broken_link_without_creating_other_targets() {
local fixture_root package_dir home_dir output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir/.local/bin"
ln -s "$fixture_root/missing-cli" "$home_dir/.local/bin/proxy-client"
if output="$(
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
)"; then
fail "Expected broken target link to be rejected"
fi
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|| fail "Expected broken CLI link in output"
[ -L "$home_dir/.local/bin/proxy-client" ] \
|| fail "Expected broken CLI link to stay unchanged"
assert_not_exists "$home_dir/.config"
assert_not_exists "$home_dir/.bashrc"
}
test_preserves_machine_files_with_both_methods() {
local stow_bin fixture_root package_dir home_dir
for stow_bin in missing-stow stow; do
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir/.config/proxy-client"
printf '%s\n' 'HOME_CONFIG_SENTINEL' \
> "$home_dir/.config/proxy-client/config"
printf '%s\n' 'HOME_PROFILE_SENTINEL' \
> "$home_dir/.config/proxy-client/default.url"
run_package_install "$package_dir" "$home_dir" "$stow_bin" \
--no-shell-setup >/dev/null
[ ! -L "$home_dir/.config/proxy-client/config" ] \
|| fail "Machine config must not become a symlink"
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|| fail "Private profile must not become a symlink"
grep -Fq 'HOME_CONFIG_SENTINEL' "$home_dir/.config/proxy-client/config" \
|| fail "Machine config changed"
grep -Fq 'HOME_PROFILE_SENTINEL' "$home_dir/.config/proxy-client/default.url" \
|| fail "Private profile changed"
done
}
test_accepts_existing_links_when_method_changes() {
local fixture_root package_dir home_dir
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
run_package_install "$package_dir" "$home_dir" missing-stow \
--no-shell-setup >/dev/null
run_package_install "$package_dir" "$home_dir" stow \
--no-shell-setup >/dev/null
assert_link_points_to \
"$home_dir/.local/bin/proxy-client" \
"$package_dir/.local/bin/proxy-client"
assert_link_points_to \
"$home_dir/.config/proxy-client/shell.bash" \
"$package_dir/.config/proxy-client/shell.bash"
assert_link_points_to \
"$home_dir/.config/proxy-client/examples" \
"$package_dir/.config/proxy-client/examples"
}
test_stow_completes_partial_correct_installation() {
local fixture_root package_dir home_dir
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
run_package_install "$package_dir" "$home_dir" missing-stow \
--no-shell-setup >/dev/null
rm "$home_dir/.config/proxy-client/shell.bash"
run_package_install "$package_dir" "$home_dir" stow \
--no-shell-setup >/dev/null
assert_link_points_to \
"$home_dir/.local/bin/proxy-client" \
"$package_dir/.local/bin/proxy-client"
assert_link_points_to \
"$home_dir/.config/proxy-client/shell.bash" \
"$package_dir/.config/proxy-client/shell.bash"
assert_link_points_to \
"$home_dir/.config/proxy-client/examples" \
"$package_dir/.config/proxy-client/examples"
}
test_does_not_duplicate_full_dotfiles_shell_setup() {
local fixture_root package_dir home_dir source_count
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
printf '%s\n' \
'[ -f ~/.config/proxy-client/shell.bash ] && source ~/.config/proxy-client/shell.bash' \
> "$home_dir/.bash_custom"
printf '%s\n' '[ -f ~/.bash_custom ] && source ~/.bash_custom' \
> "$home_dir/.bashrc"
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
grep -Fq '# >>> proxy-client >>>' "$home_dir/.bashrc" \
|| fail "Expected managed PATH block with full dotfiles"
grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc" \
|| fail "Expected ~/.local/bin setup with full dotfiles"
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc" || true)"
[ "$source_count" -eq 0 ] \
|| fail "Standalone block duplicated full dotfiles module source"
}
test_rejects_damaged_managed_bash_block_before_changes() {
local fixture_root package_dir home_dir output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
printf '%s\n' '# >>> proxy-client >>>' > "$home_dir/.bashrc"
if output="$(
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
)"; then
fail "Expected damaged managed Bash block to be rejected"
fi
printf '%s' "$output" | grep -Fq "$home_dir/.bashrc" \
|| fail "Expected ~/.bashrc conflict in output"
[ "$(wc -l < "$home_dir/.bashrc")" -eq 1 ] \
|| fail "Expected damaged Bash block to stay unchanged"
assert_not_exists "$home_dir/.local"
assert_not_exists "$home_dir/.config"
}
test_does_not_duplicate_existing_local_bin_path() {
local fixture_root package_dir home_dir path_count source_count
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' > "$home_dir/.bashrc"
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc")"
[ "$path_count" -eq 1 ] || fail "Existing ~/.local/bin PATH line was duplicated"
[ "$source_count" -eq 1 ] || fail "Expected shell module source in managed block"
}
test_rejects_incomplete_source_before_changes() {
local fixture_root package_dir home_dir output
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
package_dir="$(setup_package "$fixture_root")"
home_dir="$fixture_root/home"
mkdir -p "$home_dir"
rm "$package_dir/.config/proxy-client/shell.bash"
if output="$(
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
)"; then
fail "Expected incomplete source package to be rejected"
fi
printf '%s' "$output" | grep -Fq 'исходного пакета' \
|| fail "Expected source package error"
assert_not_exists "$home_dir/.local"
assert_not_exists "$home_dir/.config"
assert_not_exists "$home_dir/.bashrc"
}
main() {
test_installs_same_public_artifacts_without_stow
test_configures_bash_once_by_default
test_installs_same_public_artifacts_with_stow
test_rejects_linked_parent_directories_before_changes
test_reports_all_target_conflicts_without_partial_install
test_rejects_broken_link_without_creating_other_targets
test_preserves_machine_files_with_both_methods
test_accepts_existing_links_when_method_changes
test_stow_completes_partial_correct_installation
test_does_not_duplicate_full_dotfiles_shell_setup
test_rejects_damaged_managed_bash_block_before_changes
test_does_not_duplicate_existing_local_bin_path
test_rejects_incomplete_source_before_changes
echo "All standalone proxy-client install tests passed"
}
main "$@"