Files
3X-UI_autoVPS/scripts/kit-local.sh
T

487 lines
25 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 3X-UI_KIT (форк hacker07) — локальные дополнения.
#
# Этот файл наш: автор его не знает и не правит, поэтому обновления автора его не ломают.
# kit подключает его одной строкой из kit.sh (шов в tools/fork_apply.py) и передаёт сюда
# аргументы командной строки. Всё, что мы дописываем к kit, живёт ЗДЕСЬ.
#
# Команды: kit local <команда> (справка: kit local help)
#
# Часть команд повторяет поведение проекта StressOzz/vps-setup (добавить порт SSH, отключить
# ответ на ping, сменить пароль root). Там нет файла лицензии, поэтому код свой, а поведение
# сделано безопаснее, чем в источнике:
# * конфиг sshd проверяется (`sshd -t`) ДО перезапуска, новый порт сначала открывается в ufw;
# * прежний порт продолжает работать, пока новый не проверен (`--close-old` — отдельный шаг);
# * у каждой правки есть --dry-run и копия затронутого файла в /root/kit-local-backup-<дата>/;
# * apt full-upgrade из источника НЕ выполняется (он тянет систему вперёд и может сломать
# закреплённые версии панели и ядра) — вместо него автообновления безопасности.
#
# Правила работы с файлом: только функции, никакого кода на верхнем уровне (он выполняется при
# каждом запуске kit), и никаких зависимостей от функций автора: наш вызов идёт в самом начале
# kit.sh, когда авторские say/warn ещё не объявлены.
KL_BACKUP_DIR="/root/kit-local-backup-$(date +%Y%m%d)"
KL_SSHD_CONF="/etc/ssh/sshd_config"
KL_SSHD_DROPIN="/etc/ssh/sshd_config.d/99-kit-local-port.conf"
KL_SOCKET_DROPIN="/etc/systemd/system/ssh.socket.d/99-kit-local-port.conf"
KL_UFW_RULES=/etc/ufw/before.rules
KL_UFW6_RULES=/etc/ufw/before6.rules
KL_SYSCTL_DROPIN=/etc/sysctl.d/99-kit-local-icmp.conf
kl_say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
kl_ok() { printf ' ✓ %s\n' "$*"; }
kl_warn() { printf ' ! %s\n' "$*" >&2; }
kl_step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
kl_die() { printf '✗ %s\n' "$*" >&2; return 1; }
kl_have() { command -v "$1" >/dev/null 2>&1; }
kl_dry() { [[ ${KL_DRY:-no} == yes ]]; }
kl_run() { if kl_dry; then printf ' (dry-run) %s\n' "$*"; return 0; fi; "$@"; }
# Разбор аргументов: --dry-run поднимаем в KL_DRY, остальное складываем в KL_REST.
kl_args() {
KL_DRY=no; KL_REST=()
local a
for a in "$@"; do
case $a in
--dry-run) KL_DRY=yes ;;
*) KL_REST+=("$a") ;;
esac
done
}
kl_backup() { # файл → копия рядом с релизом, не в /tmp (там её переживёт только до перезагрузки)
[[ -e $1 ]] || return 0
local dst="$KL_BACKUP_DIR/$(printf '%s' "$1" | tr '/' '_')"
if kl_dry; then printf ' (dry-run) сохранил бы %s → %s\n' "$1" "$dst"; return 0; fi
install -d -m 700 "$KL_BACKUP_DIR"
cp -p "$1" "$dst" && printf ' ✓ копия: %s\n' "$dst"
return 0
}
kl_need_root() { [[ ${EUID:-0} -eq 0 ]] || kl_die "нужен root"; }
# ------------------------------------------------------------------ справка
kit_local_help() {
cat <<'EOF'
3X-UI KIT — дополнения форка (hacker07):
Состояние
kit local эта справка
kit local version версия kit, источник обновлений, дата сборки нашего файла
kit local channel откуда берутся обновления и чем проверяется подпись
kit local seams какие места форк правит в файлах автора
kit local masking состояние маскировки (у REALITY должен быть свой домен)
kit local hardening сводка по защите сервера и что ещё можно включить
Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее)
kit local ssh 2222 добавить порт SSH (прежние продолжают работать)
kit local ssh --status какие порты SSH слушаются сейчас
kit local ssh --close-old 2222 закрыть прежние порты после проверки нового
kit local ping off|on отключить/вернуть ответ на ICMP echo (ping) через ufw
kit local ping status состояние ответа на ping
kit local passwd [юзер] сменить пароль (спросит дважды, в логи не попадает)
kit local passwd --generate сгенерировать пароль и показать один раз
kit local upgrades on|off|status автообновления безопасности
У любой правки можно добавить --dry-run: покажет план и ничего не изменит.
Копии изменённых файлов — в /root/kit-local-backup-<дата>/.
Оговорки:
* прежний порт SSH сам не закрывается: сначала проверьте новый из отдельного окна;
* ping отключаем правилом ufw (видно в `ufw status`), а не общесистемным sysctl: отменяется
одной командой и не глушит диагностику для внутренних интерфейсов;
* apt full-upgrade из источника не делаем: вместо него — обновления безопасности.
EOF
}
kit_local_version() {
printf 'kit: %s (форк hacker07)\n' "${KIT_VERSION:-?}"
printf 'источник: %s\n' "${KIT_RAW:-?}"
local lib=${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}
printf 'наш файл: %s\n' "$lib"
[[ -r $lib ]] && printf 'собран: %s\n' "$(date -r "$lib" '+%Y-%m-%d %H:%M' 2>/dev/null || echo '?')"
local latest
latest=$(cat /etc/kit/latest-version 2>/dev/null || true)
[[ -n $latest ]] && printf 'последняя: %s\n' "$latest"
return 0
}
kit_local_channel() {
printf 'файлы релиза: %s\n' "${KIT_RAW:-?}"
printf 'подпись: id=%s namespace=%s\n' "${KIT_SIG_ID:-?}" "${KIT_SIG_NS:-?}"
local n=0 k
for k in "${KIT_SIGNERS[@]:-}"; do [[ -n $k ]] && n=$((n + 1)); done
printf 'ключей в KIT_SIGNERS: %d (должен быть только ключ форка)\n' "$n"
if [[ $n -eq 0 ]]; then
printf 'ВНИМАНИЕ: список ключей пуст — обновление не пройдёт проверку подписи.\n'
fi
return 0
}
kit_local_seams() {
local bin=/usr/local/bin/kit
[[ -r $bin ]] || { printf 'нет %s\n' "$bin"; return 1; }
grep -n '3X-UI_KIT (форк hacker07)' "$bin" 2>/dev/null \
|| printf 'метки форка в %s не найдены — сборка не наша?\n' "$bin"
return 0
}
kit_local_masking() {
kl_have kit || { printf 'нет команды kit\n'; return 1; }
kit net site 2>/dev/null || printf 'посмотреть: kit net site\n'
return 0
}
# ------------------------------------------------------------------ SSH: порты
kl_ssh_ports() {
{ sshd -T 2>/dev/null | awk '$1 == "port" {print $2}'
kl_have ss && ss -H -ltnp 2>/dev/null | awk '/sshd/ {n = split($4, a, ":"); print a[n]}'
[[ -n ${SSH_CONNECTION:-} ]] && awk '{print $4}' <<<"$SSH_CONNECTION"
[[ -n ${SSH_CLIENT:-} ]] && awk '{print $2}' <<<"$SSH_CLIENT"
} 2>/dev/null | grep -E '^[0-9]+$' | sort -un | tr '\n' ' '
}
kl_ssh_socket_active() { kl_have systemctl && systemctl is-active --quiet ssh.socket 2>/dev/null; }
kl_ufw_active() { kl_have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; }
kl_ssh_status() {
local ports; ports=$(kl_ssh_ports)
printf 'порты SSH сейчас: %s\n' "${ports:-не удалось определить}"
if kl_ssh_socket_active; then
printf 'слушает: systemd (ssh.socket — так в Ubuntu 24.04)\n'
[[ -f $KL_SOCKET_DROPIN ]] && grep -H 'ListenStream' "$KL_SOCKET_DROPIN"
else
printf 'слушает: sshd по %s\n' "$KL_SSHD_CONF"
[[ -f $KL_SSHD_DROPIN ]] && grep -H '^Port' "$KL_SSHD_DROPIN"
fi
kl_ufw_active && printf 'ufw: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" && $1 ~ /\/tcp$/ {printf "%s ", $1}')"
return 0
}
kit_local_ssh() {
kl_args "$@"
local arg=${KL_REST[0]:-} second=${KL_REST[1]:-} close_old=no new
[[ $arg == --close-old ]] && { close_old=yes; new=$second; }
[[ $second == --close-old ]] && close_old=yes
if [[ -z $arg || $arg == --status ]]; then kl_ssh_status; return 0; fi
kl_need_root || return 1
[[ $close_old == no ]] && new=$arg
[[ $new =~ ^[0-9]+$ ]] && ((new >= 1 && new <= 65535)) \
|| { kl_die "порт — число 1..65535, например: kit local ssh 2222"; return 1; }
local cur; cur=$(kl_ssh_ports)
if [[ $close_old == yes ]]; then
kl_say "закрываю прежние порты SSH, оставляю только $new"
printf ' было: %s\n' "${cur:-?}"
if [[ -z $cur ]]; then
kl_warn "не удалось определить текущие порты SSH — закройте прежний порт вручную (файл sshd или ssh.socket)"
return 1
fi
if kl_ssh_socket_active; then
cat <<EOF
! SSH слушает systemd (ssh.socket): закрытие старых портов делается правкой
$KL_SOCKET_DROPIN — первой строкой там должен идти «ListenStream=» (сброс списка),
затем «ListenStream=$new», после чего: systemctl daemon-reload && systemctl restart ssh.socket.
Автоматически не делаю: ошибка здесь запирает доступ к серверу.
EOF
return 0
fi
local p
for p in $cur; do
[[ $p == "$new" ]] && continue
kl_backup "$KL_SSHD_CONF"
if kl_dry; then
printf ' (dry-run) закрыл бы порт %s в %s и снял правило ufw %s/tcp\n' "$p" "$KL_SSHD_CONF" "$p"
continue
fi
sed -i -E "s|^[[:space:]]*Port[[:space:]]+$p[[:space:]]*$|# Port $p (закрыт kit local $(date +%F))|" "$KL_SSHD_CONF"
kl_ufw_active && ufw delete allow "$p/tcp" >/dev/null 2>&1
kl_ok "порт $p закрыт"
done
kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — верните файл из $KL_BACKUP_DIR"; return 1; }
kl_run systemctl restart ssh
kl_say "остался только порт $new. Проверьте вход в НОВОМ окне, это не закрывая."
return 0
fi
kl_say "добавляю порт SSH $new (прежние: ${cur:-?})"
if [[ " $cur " == *" $new "* ]]; then kl_warn "порт $new уже слушается — ничего не меняю"; return 0; fi
if kl_have ss && ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then
kl_die "порт $new занят другой службой — выберите другой"; return 1
fi
if kl_ufw_active; then
kl_run ufw allow "$new/tcp" && kl_ok "ufw: открыт $new/tcp — сделано до перезапуска, чтобы не запереть себя"
else
kl_warn "ufw не активен: проверьте, что порт $new открыт у хостера"
fi
if kl_ssh_socket_active; then
kl_backup "$KL_SOCKET_DROPIN"
if kl_dry; then
printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new"
else
install -d -m 755 "$(dirname "$KL_SOCKET_DROPIN")"
printf '# 3X-UI KIT (форк): дополнительный порт SSH, добавлен %s\n[Socket]\nListenStream=%s\n' "$(date +%F)" "$new" >"$KL_SOCKET_DROPIN"
kl_ok "добавлен $KL_SOCKET_DROPIN"
fi
kl_run systemctl daemon-reload
kl_run systemctl restart ssh.socket
else
kl_backup "$KL_SSHD_DROPIN"
if kl_dry; then
printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new"
else
install -d -m 755 /etc/ssh/sshd_config.d
printf '# 3X-UI KIT (форк): дополнительный порт SSH, добавлен %s\nPort %s\n' "$(date +%F)" "$new" >"$KL_SSHD_DROPIN"
kl_ok "добавлен $KL_SSHD_DROPIN"
fi
kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — убираю наш файл, ничего не перезапускаю"; kl_dry || rm -f "$KL_SSHD_DROPIN"; return 1; }
kl_run systemctl restart ssh || kl_run systemctl restart sshd
fi
if ! kl_dry; then
sleep 2
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then kl_ok "sshd слушает $new"
else kl_warn "порт $new пока не слушается — смотрите: journalctl -u ssh -n 30"; fi
fi
cat <<EOF
Дальше обязательно:
1) во ВТОРОМ окне проверьте вход: ssh -p $new root@<IP сервера>
2) заработало — закройте прежние порты: kit local ssh --close-old $new
3) поменяйте порт у себя в ~/.ssh/config и в мониторинге, если он там есть
EOF
return 0
}
# ------------------------------------------------------------------ ping (ICMP echo)
kl_ping_state() {
if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then
grep -qE '^-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT' "$KL_UFW_RULES" && echo allow || echo deny
elif [[ -f $KL_SYSCTL_DROPIN ]]; then
grep -q 'icmp_echo_ignore_all = 1' "$KL_SYSCTL_DROPIN" && echo deny || echo allow
else
echo "?"
fi
}
kit_local_ping() {
kl_args "$@"
local want=${KL_REST[0]:-status}
case $want in
status | "")
printf 'ответ на ping: %s\n' "$(kl_ping_state)"
if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then
printf ' ufw before.rules: echo-request %s\n' "$(grep -qE '^# -A ufw-before-input -p icmp --icmp-type echo-request' "$KL_UFW_RULES" && echo 'закрыт (строка закомментирована)' || echo 'открыт')"
[[ -f $KL_UFW6_RULES ]] && printf ' ufw before6.rules: echo-request %s\n' "$(grep -qE '^# -A ufw6-before-input' "$KL_UFW6_RULES" && echo 'закрыт' || echo 'открыт')"
fi
printf ' sysctl icmp_echo_ignore_all=%s\n' "$(sysctl -n net.ipv4.icmp_echo_ignore_all 2>/dev/null || echo '?')"
return 0 ;;
off | on) ;;
*) kl_die "kit local ping off|on|status"; return 1 ;;
esac
kl_need_root || return 1
if kl_ufw_active; then
kl_backup "$KL_UFW_RULES"; kl_backup "$KL_UFW6_RULES"
if kl_dry; then
printf ' (dry-run) %s в %s и before6.rules правило «icmp echo-request», затем ufw reload\n' \
"$([[ $want == off ]] && echo 'закомментировал бы' || echo 'вернул бы')" "$KL_UFW_RULES"
return 0
fi
local f
for f in "$KL_UFW_RULES" "$KL_UFW6_RULES"; do
[[ -f $f ]] || continue
if [[ $want == off ]]; then
sed -i -E 's|^(-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT)|# \1 # kit local: закрыт|' "$f"
else
sed -i -E 's|^# (-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT).*$|\1|' "$f"
fi
done
ufw reload >/dev/null 2>&1 || kl_warn "ufw reload не прошёл — проверьте: ufw status"
rm -f "$KL_SYSCTL_DROPIN"
if [[ $want == off ]]; then
kl_ok "сервер больше не отвечает на ping"
kl_warn "ICMP-мониторинг этого сервера из дома работать перестанет (проба «ping» даст сбой)"
kl_warn "вернуть: kit local ping on"
else
kl_ok "сервер снова отвечает на ping"
fi
return 0
fi
kl_warn "ufw не активен — использую общесистемный запрет ICMP echo (это шире: глушит ping и для внутренних интерфейсов)"
if kl_dry; then
printf ' (dry-run) записал бы %s с icmp_echo_ignore_all=%s\n' "$KL_SYSCTL_DROPIN" "$([[ $want == off ]] && echo 1 || echo 0)"
return 0
fi
printf 'net.ipv4.icmp_echo_ignore_all = %s\n' "$([[ $want == off ]] && echo 1 || echo 0)" >"$KL_SYSCTL_DROPIN"
sysctl -p "$KL_SYSCTL_DROPIN" >/dev/null 2>&1
kl_ok "готово (файл $KL_SYSCTL_DROPIN); вернуть: kit local ping on"
return 0
}
# ------------------------------------------------------------------ пароль
kit_local_passwd() {
kl_args "$@"
local user=${KL_REST[0]:-root} gen=no
[[ $user == --generate ]] && { user=root; gen=yes; }
[[ ${KL_REST[1]:-} == --generate ]] && gen=yes
kl_need_root || return 1
id "$user" >/dev/null 2>&1 || { kl_die "нет пользователя $user"; return 1; }
if [[ $gen == yes ]]; then
if kl_dry; then printf ' (dry-run) сгенерировал бы пароль пользователю %s и показал бы один раз\n' "$user"; return 0; fi
local pw; pw=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-20)
printf '%s:%s\n' "$user" "$pw" | chpasswd || { kl_die "chpasswd не сработал"; return 1; }
cat <<EOF
Пароль пользователя $user сменён. Показываю ОДИН раз:
$pw
Перенесите в CREDENTIALS.md и очистите прокрутку терминала (clear).
EOF
else
kl_say "меняю пароль $user — спросит дважды, в историю команд не попадёт"
if kl_dry; then printf ' (dry-run) запустил бы passwd %s\n' "$user"; return 0; fi
passwd "$user" || return 1
fi
kl_dry || passwd -S "$user" 2>/dev/null
sshd -T 2>/dev/null | grep -qi '^passwordauthentication no' \
&& kl_warn "в sshd вход по паролю выключен — пароль пригодится только для консоли хостера"
return 0
}
# ------------------------------------------------------------------ автообновления безопасности
kit_local_upgrades_status() {
printf 'автообновления безопасности: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)"
kl_have systemctl && printf ' служба unattended-upgrades: %s\n' "$(systemctl is-active unattended-upgrades 2>/dev/null || echo 'нет')"
[[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && sed -n '1,4p' /etc/apt/apt.conf.d/20auto-upgrades | sed 's/^/ /'
if [[ -f /var/log/unattended-upgrades/unattended-upgrades.log ]]; then
printf ' последние запуски:\n'
tail -n 3 /var/log/unattended-upgrades/unattended-upgrades.log | cut -c1-110 | sed 's/^/ /'
fi
return 0
}
kit_local_upgrades() {
kl_args "$@"
local want=${KL_REST[0]:-status}
case $want in
status | "") kit_local_upgrades_status; return 0 ;;
on | off) ;;
*) kl_die "kit local upgrades on|off|status"; return 1 ;;
esac
kl_need_root || return 1
if [[ $want == off ]]; then
kl_say "выключаю автообновления безопасности"
kl_run rm -f /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/52-kit-local-upgrades
kl_run systemctl disable --now unattended-upgrades
kl_ok "готово (пакет оставлен)"
return 0
fi
kl_say "включаю автообновления безопасности (только security, без перезагрузки)"
if ! kl_dry && ! dpkg -s unattended-upgrades >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades \
|| { kl_die "пакет не поставился"; return 1; }
fi
if kl_dry; then
printf ' (dry-run) записал бы /etc/apt/apt.conf.d/20auto-upgrades и 52-kit-local-upgrades, включил бы службу\n'
return 0
fi
cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
EOF
cat >/etc/apt/apt.conf.d/52-kit-local-upgrades <<'EOF'
// 3X-UI KIT (форк): только обновления безопасности, никакой самовольной перезагрузки.
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
EOF
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades"
kl_ok "включено: обновления безопасности ставятся сами"
kl_ok "панель и ядро это не затрагивает — они ставятся не из apt, а из своих релизов"
kl_warn "обновление ядра применится только после перезагрузки, сам сервер не перезагружается"
return 0
}
# ------------------------------------------------------------------ сводка
kit_local_hardening() {
kl_args "$@"
local ports; ports=$(kl_ssh_ports)
kl_step "Защита сервера — состояние"
printf 'порты SSH: %s\n' "${ports:-?}$(kl_ssh_socket_active && echo ' — через systemd ssh.socket')"
printf 'ответ на ping: %s\n' "$(kl_ping_state)"
printf 'ufw: %s\n' "$(kl_ufw_active && echo 'активен' || echo 'не активен')"
kl_ufw_active && printf 'открытые порты: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" {printf "%s ", $1}')"
printf 'автообновления: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)"
printf 'вход по паролю в sshd: %s\n' "$(sshd -T 2>/dev/null | awk '/^passwordauthentication/ {print $2}' || echo '?')"
printf 'смена пароля root: %s\n' "$(chage -l root 2>/dev/null | awk -F': ' '/Last password change/ {print $2}')"
printf 'продление сертификата: %s\n' "$(crontab -l 2>/dev/null | grep -c acme) задач acme в cron"
cat <<'EOF'
Чего здесь НЕТ и почему (в отличие от vps-setup):
* apt full-upgrade — тянет систему вперёд и может сломать закреплённые панель и ядро;
вместо него включены обновления безопасности;
* автоматическая перезагрузка после правок — сервер не должен вставать без вашего решения;
* общесистемный icmp_echo_ignore_all — правило ufw видно в `ufw status` и отменяется одной
командой, не глушит диагностику внутренних интерфейсов.
Что включить при желании:
kit local ssh 2222 добавить порт SSH
kit local ping off не отвечать на ping
kit local upgrades on обновления безопасности
kit local passwd --generate сменить пароль root
EOF
return 0
}
kit_local_cmd() {
case ${1:-help} in
help | -h | --help) kit_local_help ;;
version | -v) kit_local_version ;;
channel) kit_local_channel ;;
seams) kit_local_seams ;;
masking | mask) kit_local_masking ;;
ssh | ssh-port) shift; kit_local_ssh "$@" ;;
ping | icmp) shift; kit_local_ping "$@" ;;
passwd | pass) shift; kit_local_passwd "$@" ;;
upgrades | auto) shift; kit_local_upgrades "$@" ;;
hardening | hard) shift; kit_local_hardening "$@" ;;
*)
printf 'Неизвестная команда форка: %s\n' "$1" >&2
kit_local_help >&2
return 2
;;
esac
}
# Точка входа: вызывается из kit.sh с аргументами командной строки.
# Если команда не наша — возвращаем 0, и kit работает ровно как раньше.
kit_local_dispatch() {
case ${1:-} in
local)
shift
kit_local_cmd "$@"
exit $?
;;
*) return 0 ;;
esac
}