Compare commits
5
Commits
c49c9a480b
...
4b7fe4cf2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7fe4cf2f | ||
|
|
bc89e8e532 | ||
|
|
3d231ae233 | ||
|
|
1170564afc | ||
|
|
710dfdfd2d |
+40
-14
@@ -3,22 +3,48 @@ description: "Системный администратор — ОС, systemd,
|
||||
пакеты, бэкапы, логи. Каждое изменение — после одобрения детального плана
|
||||
с бэкапом, тестом и откатом."
|
||||
mode: all
|
||||
temperature: 0.1
|
||||
request:
|
||||
body:
|
||||
temperature: 0.1
|
||||
steps: 100
|
||||
color: "#16a34a"
|
||||
permission:
|
||||
task: deny
|
||||
bash: allow
|
||||
read: allow
|
||||
edit: allow
|
||||
write: allow
|
||||
grep: allow
|
||||
glob: allow
|
||||
websearch: allow
|
||||
webfetch: allow
|
||||
todowrite: allow
|
||||
question: allow
|
||||
skill: allow
|
||||
permissions:
|
||||
- action: subagent
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: read
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: edit
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: write
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: grep
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: glob
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: todowrite
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: question
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: skill
|
||||
resource: "*"
|
||||
effect: allow
|
||||
---
|
||||
|
||||
# Системный администратор
|
||||
|
||||
+40
-14
@@ -3,22 +3,48 @@ description: "DevOps-инженер — CI/CD, контейнеры, IaC, деп
|
||||
Готовит изменения как код и проверяет их dry-run. Применение и коммиты —
|
||||
только за пользователем."
|
||||
mode: all
|
||||
temperature: 0.1
|
||||
request:
|
||||
body:
|
||||
temperature: 0.1
|
||||
steps: 100
|
||||
color: "#2563eb"
|
||||
permission:
|
||||
task: deny
|
||||
bash: allow
|
||||
read: allow
|
||||
edit: allow
|
||||
write: allow
|
||||
grep: allow
|
||||
glob: allow
|
||||
websearch: allow
|
||||
webfetch: allow
|
||||
todowrite: allow
|
||||
question: allow
|
||||
skill: allow
|
||||
permissions:
|
||||
- action: subagent
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: read
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: edit
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: write
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: grep
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: glob
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: todowrite
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: question
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: skill
|
||||
resource: "*"
|
||||
effect: allow
|
||||
---
|
||||
|
||||
# DevOps-инженер
|
||||
|
||||
+3
-1
@@ -2,7 +2,9 @@
|
||||
mode: all
|
||||
description: "Bug Fix — находит причину бага, чинит, проверяет что не сломал
|
||||
остальное. Работает строго по циклу: Investigate → Fix → Verify."
|
||||
temperature: 0.2
|
||||
request:
|
||||
body:
|
||||
temperature: 0.2
|
||||
steps: 100
|
||||
color: "#800080"
|
||||
---
|
||||
|
||||
+47
-16
@@ -2,24 +2,54 @@
|
||||
description: Оркестратор — получает задачу, выбирает профиль, делегирует
|
||||
сабагентам. Сам код НЕ правит. Координирует выполнение и собирает результат.
|
||||
mode: primary
|
||||
temperature: 0.15
|
||||
request:
|
||||
body:
|
||||
temperature: 0.15
|
||||
steps: 200
|
||||
color: "#ffd43b"
|
||||
permission:
|
||||
task: allow
|
||||
bash: allow
|
||||
read: allow
|
||||
edit: allow
|
||||
write: allow
|
||||
websearch: allow
|
||||
webfetch: allow
|
||||
todowrite: allow
|
||||
skill: allow
|
||||
question: allow
|
||||
plan_enter: allow
|
||||
plan_exit: allow
|
||||
grep: allow
|
||||
glob: allow
|
||||
permissions:
|
||||
- action: subagent
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: read
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: edit
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: write
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: todowrite
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: skill
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: question
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: plan_enter
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: plan_exit
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: grep
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: glob
|
||||
resource: "*"
|
||||
effect: allow
|
||||
---
|
||||
|
||||
# Orchestrator — оркестратор задач
|
||||
@@ -52,6 +82,7 @@ permission:
|
||||
3. **Делегировать** — вызови сабагента через `task`, передав:
|
||||
- контекст задачи (файлы, ошибки, ожидания)
|
||||
- чёткий критерий «сделано»
|
||||
- по возможности запускай сабагентов в фоне, чтобы не блокировать основной диалог
|
||||
4. **Принять результат** — дождись завершения, проверь что критерии выполнены:
|
||||
- Для кода: тесты, typecheck, линтер проекта (`pytest`, `pnpm test`, `pnpm typecheck`, `pnpm lint` — что есть в проекте)
|
||||
- Для инфраструктуры: `systemctl is-active`, healthcheck, статус деплоя
|
||||
|
||||
+17
-9
@@ -2,16 +2,24 @@
|
||||
description: Исследователь и консультант по кодовой базе. Изучает архитектуру,
|
||||
находит связи, объясняет устройство проекта. Read-only.
|
||||
mode: all
|
||||
temperature: 0.2
|
||||
color: "#4ecdc4"
|
||||
permission:
|
||||
bash:
|
||||
"*": ask
|
||||
ls *: allow
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
external_directory: ask
|
||||
steps: 25
|
||||
steps: 50
|
||||
request:
|
||||
body:
|
||||
temperature: 0.2
|
||||
permissions:
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: external_directory
|
||||
resource: "*"
|
||||
effect: ask
|
||||
---
|
||||
|
||||
# Researcher — исследователь и консультант
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
description: Аудитор безопасности — read-only поиск дефектов и пробелов в защите
|
||||
по заданному вызывающим фокусу (authz/IDOR, containment путей, SSRF/egress,
|
||||
секреты, инъекции, supply-chain, публичный контракт), в режимах find и closure.
|
||||
Проект, стек, границы, чек-лист, глубина и формат артефакта задаёт вызывающий.
|
||||
mode: all
|
||||
request:
|
||||
body:
|
||||
temperature: 0.1
|
||||
steps: 60
|
||||
color: "#f59e0b"
|
||||
permissions:
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: edit
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: write
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
---
|
||||
|
||||
# Аудитор безопасности
|
||||
|
||||
Ты ищешь дефекты и пробелы в защите в заданных границах и подтверждаешь их
|
||||
доказательствами. Работаешь строго read-only: не меняешь код, файлы и конфигурации,
|
||||
не коммитишь и не пушишь.
|
||||
|
||||
## Контекст приходит от вызывающего
|
||||
|
||||
Ты не привязан к конкретному проекту. Проект, стек, язык, границы аудита,
|
||||
соглашения, фокус, чек-лист, требуемый формат артефакта и глубина проверки
|
||||
задаются в постановке задачи при вызове.
|
||||
|
||||
Пока контекст не задан — выясни его **из репозитория и постановки**, но не выдумывай:
|
||||
|
||||
- сначала прочитай, что уже есть: инструкции для разработчиков, документацию по
|
||||
безопасности и процессам, схемы конфигурации, CI-пайплайны, предыдущие отчёты;
|
||||
- если постановка противоречит тому, что видишь в репозитории — фиксируй расхождение
|
||||
как отдельную находку (декларация, расходящаяся с реальностью, — это тоже дефект);
|
||||
- если контекста не хватает для проверки (нет доступов, окружения, тестовых данных) —
|
||||
скажи об этом прямо и помечай результат как `PARTIAL`, а не догадывайся.
|
||||
|
||||
## Режимы
|
||||
|
||||
Режим задаёт вызывающий; по умолчанию — `find`.
|
||||
|
||||
- **`find`** — поиск дефектов по фокусу. Обязательного покрытия всех классов нет:
|
||||
проверяй то, что релевантно фокусу и фактической поверхности. Непроверенное
|
||||
перечисли в блоке покрытия.
|
||||
- **`closure`** — аудит фикса или инварианта. Цель — убедиться, что закрыт **класс**,
|
||||
а не одна точка: собери все точки класса, построй негативную матрицу, проверь
|
||||
наличие негативного теста на весь класс. Точечный фикс без теста на класс —
|
||||
находка.
|
||||
|
||||
## Принцип: проверка на границе
|
||||
|
||||
Для авторизации и инвариантов главная ошибка исправлений — закрыть ровно те места,
|
||||
которые назвали, и не закрыть остальные: один и тот же ресурс для одного и того же
|
||||
пользователя отвечает то разрешением, то отказом. Поэтому:
|
||||
|
||||
- **Если проверка живёт в конкретном обработчике, а не на границе (middleware,
|
||||
декоратор, единый хелпер, policy-слой) — это находка**, даже если сегодня она верна.
|
||||
- **Если закрыт один маршрут, а рядом есть такие же — это находка**: сначала собери
|
||||
*все* точки класса, потом проверяй.
|
||||
- **В режиме `closure`: если фикс не сопровождается негативным тестом на весь
|
||||
класс — это находка.** Точечный фикс класс не закрывает.
|
||||
|
||||
Это принцип для authz и инвариантов, а не требование искать «границу» в каждом классе.
|
||||
|
||||
## Классы уязвимостей: справочная карта
|
||||
|
||||
Ниже — карта типовых классов, а не обязательная программа обхода. Бери только
|
||||
релевантные фокусу и фактической поверхности; остальные явно назови как
|
||||
непроверенные. Если вызывающий требует полное покрытие — пройди все пункты и в
|
||||
блоке покрытия укажи, что проверено, а что нет и почему.
|
||||
|
||||
1. **Авторизация / доступ по идентификатору (IDOR).** Для каждого ресурса с `id`:
|
||||
где именно проверяется доступ — на границе или в обработчике. Негативная
|
||||
матрица: строки — все маршруты ресурса, столбцы — категории субъектов (чужой
|
||||
пользователь, владелец, привилегированные роли, участник коллектива,
|
||||
машинные/сервисные креды и их скоупы). Любая разрешающая клетка, которой быть не
|
||||
должно, — находка. Отдельно: нет ли маршрута без теста; не является ли «guard»
|
||||
проверкой чего-то другого (видимость, проекция скоупов), выглядя при этом
|
||||
защитой прав.
|
||||
2. **Containment путей.** Лексические проверки (сравнение `startsWith` по
|
||||
нормализованному пути) containment'ом не являются: симлинк внутри разрешённого
|
||||
каталога выводит за его пределы. Проверь наличие `realpath`/`readlink` на путях
|
||||
«вход пользователя/агента → файловая операция» (файловые инструменты, shell,
|
||||
HTTP-роуты, работа с архивами, git-подпути, защищённые пути).
|
||||
3. **SSRF / egress.** Резолвинг и проверка адреса, приватные и служебные диапазоны,
|
||||
IPv4-mapped IPv6, zone-id, редиректы, TOCTOU (адрес проверен до запроса, а запрос
|
||||
резолвит повторно). Смотри все точки исходящих запросов в границах фокуса.
|
||||
4. **Секреты.** Дерево и история VCS; соответствие того, что декларируют документы и
|
||||
CI, реальности пайплайна. Обещание сканирования, которого нет, — находка не меньшая,
|
||||
чем сам секрет. Считай историю публичной, если репозиторий зеркалируется.
|
||||
5. **Инъекции.** Shell (в т.ч. подстановки переменных, командные подстановки, glob,
|
||||
стыковка аргументов), SQL, шаблоны, HTML (XSS), десериализация, XML/XXE.
|
||||
6. **Supply-chain.** Аудит зависимостей (прямые и транзитивные, есть ли исправленные
|
||||
версии), заброшенные и лишние пакеты, deprecation, несоответствие публичного
|
||||
контракта (OpenAPI/SDK) реальному коду.
|
||||
7. **Публичный контракт как поверхность.** Всё, что отдаётся наружу (схемы, SDK,
|
||||
экспорты пакетов, эндпоинты) — потенциальная поверхность атаки и источник ложных
|
||||
обещаний.
|
||||
|
||||
Дополнительные области (конкурентность и файловые гонки, мультипроцессность,
|
||||
криптография, права в контейнере/ОС, обработка вложений) включай, если они есть в
|
||||
границах аудита.
|
||||
|
||||
## Алгоритм работы
|
||||
|
||||
1. **Понять границы и фокус**: что проверяем — весь продукт, релиз или конкретную
|
||||
подсистему; какой режим; какой артефакт и в каком формате ожидается.
|
||||
2. **Собрать поверхность машинно**: интроспекция маршрутов и публичных API, grep по
|
||||
точкам входа, список зависимостей, список мест с секретами. Не полагайся на память
|
||||
о коде.
|
||||
3. **Построить таблицу по релевантным классам**: `точка | где проверка | что
|
||||
проверяет | результат`. Незаполненная строка — кандидат в находки.
|
||||
4. **Попытаться проэксплуатировать.** Где возможно — живой запрос в подконтрольном
|
||||
окружении (изолированные данные, отдельный порт), а не рассуждение по коду.
|
||||
«Код выглядит правильно» — не проверка.
|
||||
5. **Проверить фиксы состязательно**: воспроизводит ли тест ровно тот обход, который
|
||||
был закрыт; закрой глаза на тест и попробуй снова.
|
||||
6. **Оформить результат** в формате вызывающего; если формат не задан — отчёт ниже.
|
||||
|
||||
## Формат отчёта
|
||||
|
||||
Каждая проверка — с командой и результатом:
|
||||
|
||||
```
|
||||
### Проверка: {фокус и что именно}
|
||||
**Command run:** {точная команда / точный запрос}
|
||||
**Output observed:** {фактический вывод}
|
||||
**Expected vs Actual:** {ожидание vs реальность}
|
||||
**Class:** {класс уязвимости, если применим}
|
||||
**Invariant?:** {проверка на границе | в обработчике | отсутствует | неприменимо}
|
||||
**Result: PASS | FAIL**
|
||||
```
|
||||
|
||||
Итог:
|
||||
|
||||
```
|
||||
VERDICT: PASS | FAIL | PARTIAL
|
||||
Находок: M.
|
||||
Покрытие: проверено N из K релевантных классов/проверок.
|
||||
Не проверено: {что именно} — {почему}.
|
||||
Находки, закрытые «по списку» (closure, требуют инварианта): …
|
||||
```
|
||||
|
||||
`PARTIAL` — только если проверка физически невозможна (нет окружения, доступов,
|
||||
данных); это надо явно назвать и объяснить.
|
||||
|
||||
## Что ты НЕ должен делать
|
||||
|
||||
- ❌ Менять код, создавать/удалять файлы вне разрешённого каталога, коммитить,
|
||||
пушить, править конфиги.
|
||||
- ❌ Запускать саб-агентов, входить в режим планирования.
|
||||
- ❌ Выдавать `PASS` без приведённых команд и вывода — одобрение без доказательств
|
||||
запрещено.
|
||||
- ❌ Подгонять ожидание под фактический результат: если система отвечает «разрешено»,
|
||||
а должна «отказано» — это FAIL, независимо от того, что тесты зелёные.
|
||||
- ❌ Выдумывать класс ради формата и отчитываться о классе, который не проверял.
|
||||
- ❌ Выдавать покрытие за проверку: «класс рассмотрен» без команды и вывода — не PASS.
|
||||
- ❌ Предполагать факты о проекте, которых нет в постановке или в репозитории.
|
||||
|
||||
## Дисциплина окружения
|
||||
|
||||
- Shell разрешён для чтения и проб (grep, чтение репозитория, запуск тестов и
|
||||
изолированных запросов). Команды, меняющие рабочее дерево, репозиторий или
|
||||
систему, запрещены.
|
||||
- Временные файлы (probe-скрипты, дампы ответов) — только в каталоге, который указал
|
||||
вызывающий; если не указан — `tmp/`. Ничего за его пределами.
|
||||
- Никаких установок новых инструментов и зависимостей без явного разрешения; не хватает
|
||||
инструмента — скажи об этом.
|
||||
- Эксплуатацию проводи только против подконтрольных инстансов и данных.
|
||||
+13
-5
@@ -2,13 +2,21 @@
|
||||
description: Тестировщик, QA-инженер. Находит баги, проверяет фиксы, запускает
|
||||
тесты, верифицирует что ничего не сломано. Read-only.
|
||||
mode: all
|
||||
temperature: 0.1
|
||||
request:
|
||||
body:
|
||||
temperature: 0.1
|
||||
steps: 50
|
||||
color: "#ff6b6b"
|
||||
permission:
|
||||
bash: allow
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
permissions:
|
||||
- action: shell
|
||||
resource: "*"
|
||||
effect: allow
|
||||
- action: webfetch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
- action: websearch
|
||||
resource: "*"
|
||||
effect: deny
|
||||
---
|
||||
|
||||
# Тестировщик, QA-инженер
|
||||
|
||||
Executable
+633
@@ -0,0 +1,633 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# cleanup-packages.sh — удаление orphan-пакетов pacman и старых
|
||||
# бэкендов LM Studio (Manjaro/Arch)
|
||||
#
|
||||
# Подкоманды:
|
||||
# check — состояние: что найдено, что исключено, прогноз (read-only)
|
||||
# plan — точный список того, что будет удалено (read-only)
|
||||
# apply — бэкап → план → подтверждение → удаление
|
||||
# rollback — переустановить пакеты, удалённые последним apply
|
||||
#
|
||||
# Дефолтное поведение — НИЧЕГО НЕ ДЕЛАТЬ.
|
||||
# Без аргументов скрипт печатает usage и выходит с кодом 2.
|
||||
#
|
||||
# Скрипт НЕ трогает: кэши huggingface/npm/Code, journal, перенос файлов.
|
||||
# Только две группы:
|
||||
# A — orphan-пакеты pacman (минус защищённый список из 5 пакетов)
|
||||
# B — 13 старых бэкендов LM Studio из фиксированного белого списка
|
||||
#
|
||||
# Требования к pacman (проверено на pacman 7.1.0):
|
||||
# * `pacman -R --print` печатает цели в stdout; stderr — только ошибки.
|
||||
# Перехват через 2>&1 работает в обоих случаях.
|
||||
# * `-n` (=--nosave) несовместим с `--print` → в плане только `-R`
|
||||
# * реальное удаление: `sudo pacman -Rns` (конфиги уходят в .pacsave)
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_NAME="${0##*/}"
|
||||
|
||||
# Каталог с бэкапами apply (используется и в rollback).
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/cleanup-packages"
|
||||
|
||||
# Корневой каталог бэкендов LM Studio.
|
||||
BACKENDS_ROOT="$HOME/.lmstudio/extensions/backends"
|
||||
|
||||
# ============================================================
|
||||
# Константы предметной области
|
||||
# ============================================================
|
||||
|
||||
# Пакеты-исключения: orphan'ы, которые удалять НЕЛЬЗЯ.
|
||||
# linux-meta, linux-nvidia-meta — переключатели ядра по умолчанию (0 байт)
|
||||
# linux618-nvidia — модули nvidia для LTS-ядра 6.18.55
|
||||
# (иначе чёрный экран при загрузке с 6.18)
|
||||
# python-pkg_resources — AUR-пакет, нужен pip
|
||||
# boost — нужен для сборки AUR-пакетов
|
||||
PKG_EXCLUDE_RE='^(linux-meta|linux-nvidia-meta|linux618-nvidia|python-pkg_resources|boost)$'
|
||||
|
||||
# ГРУППА B — белый список бэкендов LM Studio к удалению.
|
||||
# Зашит именно список (безопаснее, чем «оставить N самых свежих»).
|
||||
# НЕ удаляются: vendor/ (CUDA-рантайм ~1.5G) и
|
||||
# llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.67.0 (свежая рабочая).
|
||||
LM_BACKEND_NAMES=(
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.13.0"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.14.4"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.20.1"
|
||||
"llama.cpp-linux-x86_64-avx2-2.13.0"
|
||||
"llama.cpp-linux-x86_64-avx2-2.14.4"
|
||||
"llama.cpp-linux-x86_64-avx2-2.20.1"
|
||||
"llama.cpp-linux-x86_64-vulkan-avx2-2.13.0"
|
||||
"llama.cpp-linux-x86_64-vulkan-avx2-2.14.4"
|
||||
"llama.cpp-linux-x86_64-vulkan-avx2-2.20.1"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.37.0"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.51.0"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.55.0"
|
||||
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.64.0"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Вывод и утилиты
|
||||
# ============================================================
|
||||
|
||||
if [ -t 1 ]; then
|
||||
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
|
||||
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||
else
|
||||
C_RESET=''; C_BOLD=''; C_DIM=''; C_RED=''; C_GREEN=''; C_YELLOW=''
|
||||
fi
|
||||
|
||||
section() { printf '\n%s%s%s\n' "$C_BOLD" "$1" "$C_RESET"; }
|
||||
ok() { printf '%s✔%s %s\n' "$C_GREEN" "$C_RESET" "$1"; }
|
||||
warn() { printf '%s! %s%s\n' "$C_YELLOW" "$1" "$C_RESET"; }
|
||||
fail() { printf '%s✖ %s%s\n' "$C_RED" "$1" "$C_RESET"; }
|
||||
skip() { printf '%s— %s%s\n' "$C_DIM" "$1" "$C_RESET"; }
|
||||
note() { printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; }
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
Использование: $SCRIPT_NAME <команда>
|
||||
|
||||
check Состояние: найденные кандидаты, исключения, прогноз (read-only)
|
||||
plan Что ИМЕННО будет удалено + прогноз освобождения (read-only)
|
||||
apply Бэкап → план → подтверждение → удаление
|
||||
rollback Переустановить пакеты, удалённые последним apply
|
||||
|
||||
По умолчанию ничего не выполняется.
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Байты → человекочитаемый размер.
|
||||
human_size() {
|
||||
local b="${1:-0}"
|
||||
awk -v b="$b" 'BEGIN{
|
||||
split("B KiB MiB GiB TiB", u, " "); i = 1
|
||||
while (b >= 1024 && i < 5) { b = b / 1024; i = i + 1 }
|
||||
if (i == 1) printf "%d %s", b, u[i]; else printf "%.2f %s", b, u[i]
|
||||
}'
|
||||
}
|
||||
|
||||
# Размер каталога/файла в байтах (реально занятое место, без перехода по ФС).
|
||||
path_bytes() {
|
||||
local p="$1" out
|
||||
out="$(du -sx -B1 -- "$p" 2>/dev/null | awk '{print $1}' | head -n1 || true)"
|
||||
[[ "$out" =~ ^[0-9]+$ ]] || out=0
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# Свободное место на корневом разделе в байтах.
|
||||
root_free_bytes() {
|
||||
local out
|
||||
out="$(df -B1 --output=avail / 2>/dev/null | tail -n1 | tr -d ' ' || true)"
|
||||
[[ "$out" =~ ^[0-9]+$ ]] || out=0
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# Причина, по которой пакет исключён из удаления.
|
||||
exclusion_reason() {
|
||||
case "$1" in
|
||||
linux-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
|
||||
linux-nvidia-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
|
||||
linux618-nvidia) printf '%s' "драйвер nvidia для LTS-ядра 6.18 (без него чёрный экран)" ;;
|
||||
python-pkg_resources) printf '%s' "нужен для pip (AUR-пакет)" ;;
|
||||
boost) printf '%s' "нужен для сборки AUR-пакетов" ;;
|
||||
*) printf '%s' "исключён защитным списком" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Сбор данных (read-only)
|
||||
# ============================================================
|
||||
|
||||
# Все orphan-пакеты (как выдаёт pacman) — для отчёта.
|
||||
ALL_ORPHANS=()
|
||||
# Orphan'ы к удалению (минус защищённый список).
|
||||
ORPHAN_PKGS=()
|
||||
# Исключённые пакеты.
|
||||
EXCLUDED_PKGS=()
|
||||
|
||||
collect_orphan_packages() {
|
||||
ALL_ORPHANS=(); ORPHAN_PKGS=(); EXCLUDED_PKGS=()
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
ALL_ORPHANS+=("$line")
|
||||
if [[ "$line" =~ $PKG_EXCLUDE_RE ]]; then
|
||||
EXCLUDED_PKGS+=("$line")
|
||||
else
|
||||
ORPHAN_PKGS+=("$line")
|
||||
fi
|
||||
done < <(pacman -Qdtq 2>/dev/null || true)
|
||||
}
|
||||
|
||||
# Реальный вывод `pacman -R --print` (цели в формате имя-версия).
|
||||
# Внимание: pacman печатает цели в stdout, а в stderr — ошибки.
|
||||
# Перехват через 2>&1 корректен для обоих потоков.
|
||||
# `-n`/--nosave с --print несовместимы, поэтому тут только `-R`.
|
||||
print_targets() {
|
||||
((${#ORPHAN_PKGS[@]} == 0)) && return 0
|
||||
LC_ALL=C pacman -R --print "${ORPHAN_PKGS[@]}" 2>&1 || true
|
||||
}
|
||||
|
||||
# Размер каждого orphan-пакета в байтах: "имя<TAB>байты".
|
||||
# LC_ALL=C → английское поле "Installed Size" и точка как десятичный разделитель
|
||||
# (в русской локали там «Установленный размер : 151,16 MiB» с запятой).
|
||||
package_sizes() {
|
||||
((${#ORPHAN_PKGS[@]} == 0)) && return 0
|
||||
LC_ALL=C pacman -Qi "${ORPHAN_PKGS[@]}" 2>/dev/null | awk '
|
||||
/^Name[ \t]*:/ { n = $3 }
|
||||
/^Installed Size/ {
|
||||
u = $5; v = $4 + 0
|
||||
if (u == "KiB") b = v * 1024
|
||||
else if (u == "MiB") b = v * 1048576
|
||||
else if (u == "GiB") b = v * 1073741824
|
||||
else if (u == "B") b = v
|
||||
else b = 0
|
||||
printf "%s\t%d\n", n, b
|
||||
}'
|
||||
}
|
||||
|
||||
# Суммарный установленный размер orphan-пакетов (байты).
|
||||
packages_bytes() {
|
||||
package_sizes | awk '{ t += $2 } END { printf "%d", t + 0 }'
|
||||
}
|
||||
|
||||
# Бэкенды LM Studio к удалению (полные пути) и их суммарный размер.
|
||||
LM_BACKEND_PATHS=()
|
||||
LM_BACKEND_BYTES=0
|
||||
LM_BACKEND_MISSING=()
|
||||
|
||||
collect_lmstudio_backends() {
|
||||
LM_BACKEND_PATHS=(); LM_BACKEND_BYTES=0; LM_BACKEND_MISSING=()
|
||||
local name path b
|
||||
for name in "${LM_BACKEND_NAMES[@]}"; do
|
||||
path="$BACKENDS_ROOT/$name"
|
||||
if [[ ! -e "$path" ]]; then
|
||||
LM_BACKEND_MISSING+=("$name")
|
||||
continue
|
||||
fi
|
||||
if [[ ! -d "$path" || -L "$path" ]]; then
|
||||
warn "пропущен (не каталог или symlink): $path"
|
||||
LM_BACKEND_MISSING+=("$name")
|
||||
continue
|
||||
fi
|
||||
b="$(path_bytes "$path")"
|
||||
LM_BACKEND_PATHS+=("$path")
|
||||
LM_BACKEND_BYTES=$((LM_BACKEND_BYTES + b))
|
||||
done
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Проверки безопасности перед rm -rf
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Путь разрешён к удалению, только если:
|
||||
# 1) basename входит в белый список LM_BACKEND_NAMES;
|
||||
# 2) путь — каталог (не symlink, не файл);
|
||||
# 3) родитель — ровно $BACKENDS_ROOT (никаких вложенных путей).
|
||||
backend_allowed() {
|
||||
local path="$1" base parent name
|
||||
[[ -e "$path" ]] || return 1
|
||||
[[ -d "$path" ]] || return 1
|
||||
[[ ! -L "$path" ]] || return 1
|
||||
|
||||
base="$(basename -- "$path")"
|
||||
parent="$(dirname -- "$path")"
|
||||
[[ "$parent" == "$BACKENDS_ROOT" ]] || return 1
|
||||
|
||||
for name in "${LM_BACKEND_NAMES[@]}"; do
|
||||
if [[ "$base" == "$name" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# PPID процесса по /proc (comm может содержать пробелы — режем по последней ')').
|
||||
proc_ppid() {
|
||||
local pid="$1" stat rest
|
||||
[[ -r "/proc/$pid/stat" ]] || return 1
|
||||
stat="$(cat "/proc/$pid/stat" 2>/dev/null)" || return 1
|
||||
rest="${stat#*) }"
|
||||
awk '{ print $2 }' <<< "$rest"
|
||||
}
|
||||
|
||||
# Запущен ли LM Studio? (процесс не убиваем — только предупреждаем)
|
||||
# Отсекаем: сам скрипт, его предков и любой процесс, чей cmdline содержит
|
||||
# имя этого скрипта — иначе pgrep ловит сам вызов.
|
||||
lmstudio_running() {
|
||||
local pid ppid cmdline comm guard=0
|
||||
local -a ancestors=() candidates=() extra=()
|
||||
|
||||
ppid="$PPID"
|
||||
while [[ -n "$ppid" && "$ppid" != "0" && "$ppid" != "1" && $guard -lt 32 ]]; do
|
||||
ancestors+=("$ppid")
|
||||
ppid="$(proc_ppid "$ppid" || true)"
|
||||
[[ -z "$ppid" ]] && break
|
||||
guard=$((guard + 1))
|
||||
done
|
||||
|
||||
# Точное имя процесса + полный cmdline (вспомогательные процессы LM Studio).
|
||||
mapfile -t candidates < <(pgrep -x 'LM-Studio' 2>/dev/null || true)
|
||||
mapfile -t extra < <(pgrep -f 'lm-studio|LM-Studio' 2>/dev/null || true)
|
||||
candidates+=("${extra[@]:-}")
|
||||
|
||||
for pid in "${candidates[@]:-}"; do
|
||||
[[ -z "$pid" ]] && continue
|
||||
[[ "$pid" == "$$" ]] && continue
|
||||
|
||||
local is_ancestor=0 a
|
||||
for a in "${ancestors[@]:-}"; do
|
||||
if [[ -n "$a" && "$pid" == "$a" ]]; then is_ancestor=1; break; fi
|
||||
done
|
||||
[[ "$is_ancestor" == "1" ]] && continue
|
||||
|
||||
cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
|
||||
[[ "$cmdline" == *"$SCRIPT_NAME"* ]] && continue
|
||||
|
||||
comm="$(cat "/proc/$pid/comm" 2>/dev/null || true)"
|
||||
case "$comm" in
|
||||
LM-Studio*|lm-studio*|LMStudio*|lmstudio*)
|
||||
printf '%s\n' "$pid"; return 0 ;;
|
||||
esac
|
||||
case "$cmdline" in
|
||||
*LM-Studio*|*lm-studio*)
|
||||
printf '%s\n' "$pid"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Подтверждение: только точное слово "yes".
|
||||
confirm() {
|
||||
local prompt="$1" reply=""
|
||||
printf '%s' "$prompt"
|
||||
if ! IFS= read -r reply; then reply=""; fi
|
||||
[[ "$reply" == "yes" ]]
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Отчёт: check / plan
|
||||
# ============================================================
|
||||
|
||||
print_excluded() {
|
||||
((${#EXCLUDED_PKGS[@]} == 0)) && return 0
|
||||
printf ' %sИсключено (не удаляем):%s\n' "$C_BOLD" "$C_RESET"
|
||||
local p
|
||||
for p in "${EXCLUDED_PKGS[@]}"; do
|
||||
printf ' %-24s %s%s%s\n' "$p" "$C_DIM" "$(exclusion_reason "$p")" "$C_RESET"
|
||||
done
|
||||
}
|
||||
|
||||
print_plan_section() {
|
||||
local total_bytes="$1"
|
||||
|
||||
section "A. Orphan-пакеты pacman (${#ORPHAN_PKGS[@]} шт.)"
|
||||
if ((${#ORPHAN_PKGS[@]} == 0)); then
|
||||
skip "orphan-пакетов к удалению нет"
|
||||
else
|
||||
printf ' %sЦели (реальный вывод `pacman -R --print`):%s\n' "$C_BOLD" "$C_RESET"
|
||||
print_targets | sed 's/^/ /'
|
||||
printf '\n %sРазмеры:%s\n' "$C_BOLD" "$C_RESET"
|
||||
package_sizes | sort -t$'\t' -k2 -rn | awk -F'\t' '
|
||||
{ printf " %-42s %10.2f MiB\n", $1, $2 / 1048576 }'
|
||||
fi
|
||||
print_excluded
|
||||
|
||||
section "B. Старые бэкенды LM Studio (${#LM_BACKEND_PATHS[@]} шт.)"
|
||||
if ((${#LM_BACKEND_PATHS[@]} == 0)); then
|
||||
skip "бэкендов к удалению нет (уже удалены или не установлены)"
|
||||
else
|
||||
local path
|
||||
for path in "${LM_BACKEND_PATHS[@]}"; do
|
||||
printf ' %s\n' "$path"
|
||||
printf ' %s%s%s\n' "$C_DIM" "$(human_size "$(path_bytes "$path")")" "$C_RESET"
|
||||
done
|
||||
fi
|
||||
if ((${#LM_BACKEND_MISSING[@]} > 0)); then
|
||||
printf '\n %sОтсутствуют (пропущены, идемпотентность):%s\n' "$C_DIM" "$C_RESET"
|
||||
local m
|
||||
for m in "${LM_BACKEND_MISSING[@]}"; do
|
||||
printf ' %s%s%s\n' "$C_DIM" "$m" "$C_RESET"
|
||||
done
|
||||
fi
|
||||
printf '\n %sНе трогаем:%s vendor/ (CUDA-рантайм) и ...-nvidia-cuda12-avx2-2.67.0 (свежая)\n' \
|
||||
"$C_DIM" "$C_RESET"
|
||||
|
||||
section "Прогноз"
|
||||
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
|
||||
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
|
||||
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$total_bytes")" "$C_RESET"
|
||||
printf ' %sСвободно на / : %s (было %s)%s\n' \
|
||||
"$C_DIM" "$(human_size "$(root_free_bytes)")" "$(human_size "$FREE_BEFORE")" "$C_RESET"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Команды
|
||||
# ============================================================
|
||||
|
||||
cmd_check() {
|
||||
section "Проверка (read-only)"
|
||||
|
||||
printf ' orphan-пакетов найдено всего : %s\n' "${#ALL_ORPHANS[@]}"
|
||||
printf ' из них исключено : %s\n' "${#EXCLUDED_PKGS[@]}"
|
||||
printf ' к удалению (группа A) : %s\n' "${#ORPHAN_PKGS[@]}"
|
||||
print_excluded
|
||||
printf '\n старых бэкендов LM Studio : %s (из %s в белом списке)\n' \
|
||||
"${#LM_BACKEND_PATHS[@]}" "${#LM_BACKEND_NAMES[@]}"
|
||||
printf ' бэкендов отсутствует (ок) : %s\n' "${#LM_BACKEND_MISSING[@]}"
|
||||
|
||||
section "Прогноз освобождения"
|
||||
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
|
||||
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
|
||||
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$TOTAL_BYTES")" "$C_RESET"
|
||||
printf ' %sСвободно на / : %s%s\n' "$C_DIM" "$(human_size "$(root_free_bytes)")" "$C_RESET"
|
||||
printf '\n %sНичего не изменено.%s Подробности: %s plan\n' "$C_DIM" "$C_RESET" "$SCRIPT_NAME"
|
||||
}
|
||||
|
||||
cmd_plan() {
|
||||
section "План удаления (read-only)"
|
||||
print_plan_section "$TOTAL_BYTES"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# apply
|
||||
# ------------------------------------------------------------
|
||||
|
||||
cmd_apply() {
|
||||
section "APPLY — удаление orphan-пакетов и старых бэкендов LM Studio"
|
||||
|
||||
if ((${#ORPHAN_PKGS[@]} == 0 && ${#LM_BACKEND_PATHS[@]} == 0)); then
|
||||
skip "нечего удалять — всё уже чисто"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 1) Полный план.
|
||||
print_plan_section "$TOTAL_BYTES"
|
||||
|
||||
# 2) Предупреждение про запущенный LM Studio (не убиваем процесс).
|
||||
if lmstudio_running >/dev/null 2>&1; then
|
||||
warn "LM Studio запущен — закройте его перед удалением бэкендов."
|
||||
note "Процесс LM Studio не будет убит этим скриптом."
|
||||
fi
|
||||
|
||||
# 3) Подтверждение.
|
||||
printf '\n'
|
||||
if ! confirm "Удалить перечисленное? Введите 'yes' для подтверждения: "; then
|
||||
printf '\n'
|
||||
fail "Отменено пользователем. Ничего не удалено."
|
||||
return 0
|
||||
fi
|
||||
printf '\n'
|
||||
|
||||
# 4) Бэкап.
|
||||
local stamp backup_dir
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup_dir="$STATE_DIR/$stamp"
|
||||
local n=1
|
||||
while [[ -e "$backup_dir" ]]; do
|
||||
backup_dir="$STATE_DIR/$stamp-$n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
mkdir -p -- "$backup_dir"
|
||||
|
||||
printf '%s\n' "${ORPHAN_PKGS[@]:-}" > "$backup_dir/packages-removed.txt"
|
||||
local bnames=()
|
||||
local path
|
||||
for path in "${LM_BACKEND_PATHS[@]:-}"; do
|
||||
bnames+=("$(basename -- "$path")")
|
||||
done
|
||||
printf '%s\n' "${bnames[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
|
||||
# Пустые строки от "${arr[@]:-}" на пустом массиве — убираем.
|
||||
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
|
||||
|
||||
{
|
||||
printf 'cleanup-packages.sh — отчёт об удалении\n'
|
||||
printf 'Дата (UTC): %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S')"
|
||||
printf 'Дата (лок): %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf 'Хост: %s\n' "$(hostname)"
|
||||
printf '\n=== df ДО ===\n'
|
||||
df -h / 2>/dev/null || true
|
||||
printf '\n=== пакеты (имя, установленный размер) ===\n'
|
||||
package_sizes | awk -F'\t' '{ printf "%-42s %s\n", $1, $2 }'
|
||||
printf '\n=== бэкапы ===\n'
|
||||
printf 'packages-removed.txt — %s пакет(ов)\n' "${#ORPHAN_PKGS[@]}"
|
||||
printf 'lmstudio-backends-removed.txt — %s каталог(ов)\n' "${#LM_BACKEND_PATHS[@]}"
|
||||
printf '\n=== df ПОСЛЕ ===\n'
|
||||
} > "$backup_dir/manifest.txt"
|
||||
chmod 600 -- "$backup_dir"/* 2>/dev/null || true
|
||||
|
||||
ok "бэкап создан: $backup_dir"
|
||||
|
||||
# 5) Удаление бэкендов LM Studio (от пользователя, sudo не нужен).
|
||||
local removed_backends=0
|
||||
local -a removed_backend_names=()
|
||||
if ((${#LM_BACKEND_PATHS[@]} > 0)); then
|
||||
section "Удаление бэкендов LM Studio"
|
||||
for path in "${LM_BACKEND_PATHS[@]}"; do
|
||||
if backend_allowed "$path"; then
|
||||
if rm -rf -- "$path"; then
|
||||
ok "удалён: $path"
|
||||
removed_backends=$((removed_backends + 1))
|
||||
removed_backend_names+=("$(basename -- "$path")")
|
||||
else
|
||||
fail "не удалось удалить: $path"
|
||||
fi
|
||||
else
|
||||
fail "ПРОПУЩЕН (не прошёл проверку безопасности): $path"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 6) Удаление пакетов pacman (нужен sudo).
|
||||
if ((${#ORPHAN_PKGS[@]} > 0)); then
|
||||
section "Удаление orphan-пакетов pacman"
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
warn "sudo не найден — пакеты пропущены (бэкап сохранён, откат возможен)."
|
||||
elif ! sudo -v; then
|
||||
warn "sudo недоступен (не удалось получить права) — пакеты пропущены."
|
||||
note "Бэкап: $backup_dir — откат возможен командой: $SCRIPT_NAME rollback"
|
||||
else
|
||||
# -n (--nosave) оставляет конфиги в .pacsave; --noconfirm — мы уже спросили.
|
||||
if sudo pacman -Rns --noconfirm "${ORPHAN_PKGS[@]}"; then
|
||||
ok "pacman завершил удаление пакетов"
|
||||
else
|
||||
fail "pacman завершился с ошибкой — часть пакетов могла остаться"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7) Итог: df после + уточнение бэкапа (только реально удалённое).
|
||||
local free_after
|
||||
free_after="$(root_free_bytes)"
|
||||
|
||||
# Уточняем списки: оставляем только то, что действительно исчезло с системы.
|
||||
local -a removed_pkg_names=()
|
||||
local p
|
||||
for p in "${ORPHAN_PKGS[@]:-}"; do
|
||||
[[ -z "$p" ]] && continue
|
||||
if ! pacman -Q "$p" >/dev/null 2>&1; then
|
||||
removed_pkg_names+=("$p")
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "${removed_pkg_names[@]:-}" > "$backup_dir/packages-removed.txt"
|
||||
printf '%s\n' "${removed_backend_names[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
|
||||
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
|
||||
|
||||
{
|
||||
printf '\n=== df ПОСЛЕ (факт) ===\n'
|
||||
df -h / 2>/dev/null || true
|
||||
printf '\nУдалено пакетов: %s\n' "${#removed_pkg_names[@]}"
|
||||
printf 'Удалено бэкендов: %s\n' "${#removed_backend_names[@]}"
|
||||
printf 'Свободно до: %s\n' "$(human_size "$FREE_BEFORE")"
|
||||
printf 'Свободно после: %s\n' "$(human_size "$free_after")"
|
||||
printf '\n=== фактически удалённые пакеты ===\n'
|
||||
printf '%s\n' "${removed_pkg_names[@]:-}" | sed '/^$/d'
|
||||
printf '\n=== фактически удалённые бэкенды ===\n'
|
||||
printf '%s\n' "${removed_backend_names[@]:-}" | sed '/^$/d'
|
||||
} >> "$backup_dir/manifest.txt"
|
||||
|
||||
section "Готово"
|
||||
printf ' удалено пакетов : %s\n' "${#removed_pkg_names[@]}"
|
||||
printf ' удалено бэкендов : %s\n' "${#removed_backend_names[@]}"
|
||||
printf ' свободно на / : %s → %s\n' \
|
||||
"$(human_size "$FREE_BEFORE")" "$(human_size "$free_after")"
|
||||
printf ' бэкап : %s\n' "$backup_dir"
|
||||
printf '\n %sОткат: %s rollback%s\n' "$C_DIM" "$SCRIPT_NAME" "$C_RESET"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# rollback
|
||||
# ------------------------------------------------------------
|
||||
|
||||
cmd_rollback() {
|
||||
section "ROLLBACK — восстановление пакетов из последнего apply"
|
||||
|
||||
local latest
|
||||
latest="$(find "$STATE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -n1 || true)"
|
||||
if [[ -z "$latest" ]]; then
|
||||
fail "бэкапов не найдено в $STATE_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ok "последний бэкап: $latest"
|
||||
|
||||
local pkgs=()
|
||||
if [[ -f "$latest/packages-removed.txt" ]]; then
|
||||
mapfile -t pkgs < "$latest/packages-removed.txt"
|
||||
fi
|
||||
local backends=()
|
||||
if [[ -f "$latest/lmstudio-backends-removed.txt" ]]; then
|
||||
mapfile -t backends < "$latest/lmstudio-backends-removed.txt"
|
||||
fi
|
||||
|
||||
if ((${#pkgs[@]} == 0)); then
|
||||
skip "в бэкапе нет списка пакетов"
|
||||
else
|
||||
printf ' %sПакеты к переустановке (%s шт.):%s\n' "$C_BOLD" "${#pkgs[@]}" "$C_RESET"
|
||||
local p
|
||||
for p in "${pkgs[@]}"; do
|
||||
[[ -z "$p" ]] && continue
|
||||
printf ' %s\n' "$p"
|
||||
done
|
||||
printf '\n %sКоманда восстановления:%s\n' "$C_BOLD" "$C_RESET"
|
||||
printf ' sudo pacman -S %s\n' "${pkgs[*]}"
|
||||
|
||||
printf '\n'
|
||||
if confirm "Выполнить восстановление пакетов? Введите 'yes': "; then
|
||||
if sudo -v && sudo pacman -S --noconfirm "${pkgs[@]}"; then
|
||||
ok "пакеты переустановлены"
|
||||
else
|
||||
fail "восстановление не удалось"
|
||||
fi
|
||||
else
|
||||
skip "восстановление не выполнялось (команда выше — скопируйте вручную)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((${#backends[@]} > 0)); then
|
||||
printf '\n %sБэкенды LM Studio (%s шт.) — перекачиваются автоматически:%s\n' \
|
||||
"$C_BOLD" "${#backends[@]}" "$C_RESET"
|
||||
local b
|
||||
for b in "${backends[@]}"; do
|
||||
[[ -z "$b" ]] && continue
|
||||
printf ' %s\n' "$b"
|
||||
done
|
||||
note "LM Studio сам скачает нужную версию при следующем запуске."
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Точка входа
|
||||
# ============================================================
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
check|plan|apply|rollback) ;;
|
||||
-h|--help|help) usage ;;
|
||||
"") usage ;;
|
||||
*) printf 'Неизвестная команда: %s\n\n' "$cmd" >&2; usage ;;
|
||||
esac
|
||||
|
||||
if ! command -v pacman >/dev/null 2>&1; then
|
||||
fail "pacman не найден — этот скрипт рассчитан на Manjaro/Arch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
collect_orphan_packages
|
||||
collect_lmstudio_backends
|
||||
|
||||
PACKAGES_BYTES="$(packages_bytes)"
|
||||
TOTAL_BYTES=$((PACKAGES_BYTES + LM_BACKEND_BYTES))
|
||||
FREE_BEFORE="$(root_free_bytes)"
|
||||
|
||||
case "$cmd" in
|
||||
check) cmd_check ;;
|
||||
plan) cmd_plan ;;
|
||||
apply) cmd_apply ;;
|
||||
rollback) cmd_rollback ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user