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-15
Статус: реализация готова к ручной приёмке; публичное имя уточнено 2026-07-21 Статус: основная реализация принята вручную; самостоятельный установщик
спроектирован 2026-07-21, но ещё не реализован
## Проблема ## Проблема
@@ -48,8 +49,9 @@ proxy-переменных совместима с локальным Xray бе
JSON Xray. JSON Xray.
- Сохранить совместимость Bash-функций с внешним Hiddify. - Сохранить совместимость Bash-функций с внешним Hiddify.
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория. - Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
- Поддержать выборочную установку после `git clone`: через stow, ручные - Поддержать самостоятельную установку из отдельно скопированного каталога
симлинки или копирование. `proxy-client/` без зависимости от общего установщика dotfiles и без
обязательного stow.
## Не входит в объём ## Не входит в объём
@@ -89,6 +91,22 @@ Bash нужен только там, где необходимо изменит
требует кода, но не решает основную боль: безопасный запуск `pclaude` и требует кода, но не решает основную боль: безопасный запуск `pclaude` и
`pcodex` из точечной ссылки Hiddify. `pcodex` из точечной ссылки Hiddify.
### Доставка отдельного пакета
Рассмотрены четыре способа подключить отдельно скопированный каталог:
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
легко забываемые шаги;
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
но усложняет обновления и расходится с моделью dotfiles;
- требовать stow — даёт привычное управление ссылками, но добавляет
необязательную зависимость на удалённых серверах;
- дать пакету собственный установщик, который использует stow при наличии и
создаёт те же симлинки напрямую без него.
Выбран последний вариант. Он сохраняет один результат установки при разном
составе машины и не требует второй копии обновляемых файлов.
## Рекомендуемое решение ## Рекомендуемое решение
### Границы компонентов ### Границы компонентов
@@ -376,15 +394,50 @@ CLI раскладывается как `~/.local/bin/proxy-client`. Машин
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские `~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
файлы. файлы.
В корне этого каталога находится самостоятельный `README.md`. Он является В корне этого каталога находятся самостоятельный `README.md` и пакетный
основной инструкцией клиента и исключается из раскладки stow. README остаётся `install.sh`. Оба файла относятся к исходному каталогу и исключаются из
рядом с кодом при выборочном клонировании или копировании каталога. Корневой раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
README репозитория только кратко описывает подсистему и ссылается на него. копировании каталога. Корневой README репозитория только кратко описывает
подсистему и ссылается на него.
После клонирования репозитория пакет можно подключить отдельно от общего Пакетный установщик связывает отдельно скопированный каталог с домашним
`install.sh`. README также должен описывать эквивалентные ручные симлинки и каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
копирование файлов. Полный `install.sh` подключает этот же stow-пакет вместе с доступен, установщик использует его; если нет — создаёт тот же набор симлинков
остальными dotfiles и не имеет отдельной реализации. напрямую. Выбранный механизм явно показывается пользователю, но не меняет
результат. Исходный каталог должен оставаться на прежнем пути всё время
использования ссылок.
Установщик управляет только тремя публичными артефактами: 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 и создаёт - Официальный системный установщик отклонён: он требует root и создаёт
systemd-службы. Пользовательская установка соответствует выбранной модели. systemd-службы. Пользовательская установка соответствует выбранной модели.
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
усложняет обновление и нарушает общую с dotfiles модель симлинков.
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
результат на машине без stow, а автоматический выбор механизма не меняет
публичное поведение пакета.
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
установщика пакета. Эти операции зависят от машины, могут требовать секретов
или сети и уже имеют отдельные явные команды и инструкции.
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная - Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
VLESS-ссылка проще для пользователя и ограничивает область преобразования. VLESS-ссылка проще для пользователя и ограничивает область преобразования.
@@ -428,6 +489,15 @@ README репозитория только кратко описывает по
истинное значение сопровождается предупреждением. истинное значение сопровождается предупреждением.
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до - Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
основной работы и сообщает обнаруженную и требуемую версии. основной работы и сообщает обнаруженную и требуемую версии.
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
сообщает эту зависимость и при повторном запуске принимает только ссылки на
текущий источник.
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
оболочки. Установщик распознаёт собственный блок и известное подключение из
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
- Частичная установка может оставить неоднозначное состояние. Все конфликты
назначений проверяются до изменений; проверки охватывают отсутствие новых
ссылок и правок shell при отказе.
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP - `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
и домены, а CIDR описывает как совместимость best-effort. и домены, а CIDR описывает как совместимость best-effort.
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и - Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
@@ -463,6 +533,22 @@ README репозитория только кратко описывает по
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии - Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`. отсутствие автоматического запроса внешнего 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 и на одной целевой - Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
доступности: недоступная среда явно записывается как неподтверждённая, а доступности: недоступная среда явно записывается как неподтверждённая, а
@@ -474,7 +560,11 @@ README репозитория только кратко описывает по
реализацией. Она должна описывать: реализацией. Она должна описывать:
- различие внешнего Hiddify и управляемого Xray; - различие внешнего Hiddify и управляемого Xray;
- выборочная установка через stow, ручные симлинки и копирование; - самостоятельный `install.sh`, его автоматический выбор stow или прямых
симлинков, требование сохранять исходный каталог и режим
`--no-shell-setup`;
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
конфигом и закрытым профилем;
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки; - создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
- команды управления, `proxy_on/off`, `pclaude/pcodex`; - команды управления, `proxy_on/off`, `pclaude/pcodex`;
- настройка `NO_PROXY`, ограничения CIDR и диагностика; - настройка `NO_PROXY`, ограничения CIDR и диагностика;
+3
View File
@@ -1 +1,4 @@
^/README\.md$ ^/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 ### В составе dotfiles
Полная установка подключает пакет и автоматически загружает Bash-функции в Корневой установщик dotfiles подключает тот же пакет через Stow и загружает
новых интерактивных сессиях: Bash-функции в новых интерактивных сессиях:
```bash ```bash
cd ~/dotfiles cd ~/dotfiles
./install.sh ./install.sh
``` ```
Установщик заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow Он заранее создаёт `~/.local/bin` и `~/.config/proxy-client`, чтобы Stow
размещал ссылки внутри них, а не превращал сами каталоги в ссылки на размещал ссылки внутри них, а не превращал сами каталоги в ссылки на
репозиторий. репозиторий. После установки откройте новый Bash или выполните:
После установки откройте новый Bash или выполните:
```bash ```bash
export PATH="$HOME/.local/bin:$PATH"
source ~/.bash_custom source ~/.bash_custom
``` ```
### Только пакет `proxy-client` Если `~/.local/bin` отсутствует в `PATH` и в новых сессиях, добавьте ту же строку
`export` в свой Bash-конфиг: корневой установщик dotfiles его не изменяет.
```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
```
## Как выбирается прокси ## Как выбирается прокси
@@ -80,28 +107,46 @@ proxy-client status
pclaude --version pclaude --version
``` ```
В WSL NAT mode IP Windows-шлюза меняется при перезапуске. Интерактивный #### WSL NAT mode
`./install.sh` предлагает добавить обновление `hproxy.local` в `/etc/wsl.conf`.
То же можно настроить вручную:
```ini IP Windows-шлюза меняется при перезапуске WSL, поэтому `hproxy.local` нужно
[boot] обновлять. В отдельно скопированном пакете системных помощников нет. Узнайте
command = "/path/to/dotfiles/scripts/setup-hproxy.sh" адрес шлюза:
```bash
ip route show default | awk 'NR == 1 {print $3}'
``` ```
После изменения выполните `wsl --shutdown` из Windows. Обновить адрес без Затем откройте `/etc/hosts` через `sudoedit` и оставьте одну отдельную строку с
перезапуска WSL можно так: полученным адресом:
```bash
sudoedit /etc/hosts
```
```text
<адрес-шлюза> hproxy.local
```
Не удаляйте другие имена на той же строке: при необходимости перенесите их на
отдельную строку. Повторяйте обновление после перезапуска WSL.
С полным dotfiles запустите корневой `~/dotfiles/install.sh` интерактивно: он
предложит автоматическое обновление при старте WSL. Обновить адрес сразу можно
так:
```bash ```bash
sudo ~/dotfiles/scripts/setup-hproxy.sh sudo ~/dotfiles/scripts/setup-hproxy.sh
# или после загрузки Bash-функций: # после настройки автозапуска и загрузки общих Bash-функций:
update_hproxy update_hproxy
``` ```
В NAT mode Hiddify должен слушать адрес, доступный через Windows-шлюз, а не В NAT mode Hiddify должен слушать адрес, доступный через Windows-шлюз, а не
только `127.0.0.1`. только `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`. (`networkingMode=mirrored`) Windows-сервисы доступны из WSL через `127.0.0.1`.
Чтобы сохранить адрес по умолчанию, добавьте алиас в Windows hosts: Чтобы сохранить адрес по умолчанию, добавьте алиас в Windows hosts:
@@ -110,9 +155,9 @@ update_hproxy
127.0.0.1 hproxy.local 127.0.0.1 hproxy.local
``` ```
Альтернатива — создать `config.external` с `address = 127.0.0.1`. В обоих Альтернатива — настройте внешний режим по следующему разделу и укажите
случаях проверьте результат через `getent hosts hproxy.local` или `address = 127.0.0.1`. Алиас проверяйте через `getent hosts hproxy.local`, а
`proxy-client status`. активный конфиг — через `proxy-client status`.
### Другой внешний прокси ### Другой внешний прокси
@@ -158,7 +203,7 @@ chmod 600 ~/.config/proxy-client/default.url
proxy-client install proxy-client install
``` ```
Запустите Xray в отдельной панели tmux: Запустите Xray в отдельном терминале или панели tmux:
```bash ```bash
proxy-client start proxy-client start
@@ -247,20 +292,32 @@ export PROXY_PREFLIGHT_TIMEOUT=5
## Справочник ## Справочник
### Ручное подключение без Stow ### Ручное подключение без установщика
Можно создать ссылки на CLI, Bash-модуль и примеры либо скопировать те же файлы: Если пакетный установщик запустить нельзя, выберите постоянный абсолютный путь
к исходному каталогу и создайте те же ссылки вручную. Команды предполагают, что
назначения ещё не заняты:
```bash ```bash
mkdir -p ~/.local/bin ~/.config/proxy-client mkdir -p ~/.local/bin ~/.config/proxy-client
ln -s /path/to/proxy-client/.local/bin/proxy-client ~/.local/bin/proxy-client ln -s /absolute/path/to/proxy-client/.local/bin/proxy-client \
ln -s /path/to/proxy-client/.config/proxy-client/shell.bash \ ~/.local/bin/proxy-client
ln -s /absolute/path/to/proxy-client/.config/proxy-client/shell.bash \
~/.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 ~/.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 python3 -m unittest discover -s tests
bash tests/test_proxy_launchers.sh bash tests/test_proxy_launchers.sh
bash tests/test_proxy_client_install.sh
bash tests/test_install.sh bash tests/test_install.sh
uv run --python 3.8 --no-project python -m unittest discover -s tests 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 "$@"