Compare commits
5
Commits
c49c9a480b
...
4b7fe4cf2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7fe4cf2f | ||
|
|
bc89e8e532 | ||
|
|
3d231ae233 | ||
|
|
1170564afc | ||
|
|
710dfdfd2d |
+39
-13
@@ -3,22 +3,48 @@ description: "Системный администратор — ОС, systemd,
|
|||||||
пакеты, бэкапы, логи. Каждое изменение — после одобрения детального плана
|
пакеты, бэкапы, логи. Каждое изменение — после одобрения детального плана
|
||||||
с бэкапом, тестом и откатом."
|
с бэкапом, тестом и откатом."
|
||||||
mode: all
|
mode: all
|
||||||
|
request:
|
||||||
|
body:
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
steps: 100
|
steps: 100
|
||||||
color: "#16a34a"
|
color: "#16a34a"
|
||||||
permission:
|
permissions:
|
||||||
task: deny
|
- action: subagent
|
||||||
bash: allow
|
resource: "*"
|
||||||
read: allow
|
effect: deny
|
||||||
edit: allow
|
- action: shell
|
||||||
write: allow
|
resource: "*"
|
||||||
grep: allow
|
effect: allow
|
||||||
glob: allow
|
- action: read
|
||||||
websearch: allow
|
resource: "*"
|
||||||
webfetch: allow
|
effect: allow
|
||||||
todowrite: allow
|
- action: edit
|
||||||
question: allow
|
resource: "*"
|
||||||
skill: allow
|
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
|
||||||
---
|
---
|
||||||
|
|
||||||
# Системный администратор
|
# Системный администратор
|
||||||
|
|||||||
+39
-13
@@ -3,22 +3,48 @@ description: "DevOps-инженер — CI/CD, контейнеры, IaC, деп
|
|||||||
Готовит изменения как код и проверяет их dry-run. Применение и коммиты —
|
Готовит изменения как код и проверяет их dry-run. Применение и коммиты —
|
||||||
только за пользователем."
|
только за пользователем."
|
||||||
mode: all
|
mode: all
|
||||||
|
request:
|
||||||
|
body:
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
steps: 100
|
steps: 100
|
||||||
color: "#2563eb"
|
color: "#2563eb"
|
||||||
permission:
|
permissions:
|
||||||
task: deny
|
- action: subagent
|
||||||
bash: allow
|
resource: "*"
|
||||||
read: allow
|
effect: deny
|
||||||
edit: allow
|
- action: shell
|
||||||
write: allow
|
resource: "*"
|
||||||
grep: allow
|
effect: allow
|
||||||
glob: allow
|
- action: read
|
||||||
websearch: allow
|
resource: "*"
|
||||||
webfetch: allow
|
effect: allow
|
||||||
todowrite: allow
|
- action: edit
|
||||||
question: allow
|
resource: "*"
|
||||||
skill: allow
|
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-инженер
|
# DevOps-инженер
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
mode: all
|
mode: all
|
||||||
description: "Bug Fix — находит причину бага, чинит, проверяет что не сломал
|
description: "Bug Fix — находит причину бага, чинит, проверяет что не сломал
|
||||||
остальное. Работает строго по циклу: Investigate → Fix → Verify."
|
остальное. Работает строго по циклу: Investigate → Fix → Verify."
|
||||||
|
request:
|
||||||
|
body:
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
steps: 100
|
steps: 100
|
||||||
color: "#800080"
|
color: "#800080"
|
||||||
|
|||||||
+46
-15
@@ -2,24 +2,54 @@
|
|||||||
description: Оркестратор — получает задачу, выбирает профиль, делегирует
|
description: Оркестратор — получает задачу, выбирает профиль, делегирует
|
||||||
сабагентам. Сам код НЕ правит. Координирует выполнение и собирает результат.
|
сабагентам. Сам код НЕ правит. Координирует выполнение и собирает результат.
|
||||||
mode: primary
|
mode: primary
|
||||||
|
request:
|
||||||
|
body:
|
||||||
temperature: 0.15
|
temperature: 0.15
|
||||||
steps: 200
|
steps: 200
|
||||||
color: "#ffd43b"
|
color: "#ffd43b"
|
||||||
permission:
|
permissions:
|
||||||
task: allow
|
- action: subagent
|
||||||
bash: allow
|
resource: "*"
|
||||||
read: allow
|
effect: allow
|
||||||
edit: allow
|
- action: shell
|
||||||
write: allow
|
resource: "*"
|
||||||
websearch: allow
|
effect: allow
|
||||||
webfetch: allow
|
- action: read
|
||||||
todowrite: allow
|
resource: "*"
|
||||||
skill: allow
|
effect: allow
|
||||||
question: allow
|
- action: edit
|
||||||
plan_enter: allow
|
resource: "*"
|
||||||
plan_exit: allow
|
effect: allow
|
||||||
grep: allow
|
- action: write
|
||||||
glob: allow
|
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 — оркестратор задач
|
# Orchestrator — оркестратор задач
|
||||||
@@ -52,6 +82,7 @@ permission:
|
|||||||
3. **Делегировать** — вызови сабагента через `task`, передав:
|
3. **Делегировать** — вызови сабагента через `task`, передав:
|
||||||
- контекст задачи (файлы, ошибки, ожидания)
|
- контекст задачи (файлы, ошибки, ожидания)
|
||||||
- чёткий критерий «сделано»
|
- чёткий критерий «сделано»
|
||||||
|
- по возможности запускай сабагентов в фоне, чтобы не блокировать основной диалог
|
||||||
4. **Принять результат** — дождись завершения, проверь что критерии выполнены:
|
4. **Принять результат** — дождись завершения, проверь что критерии выполнены:
|
||||||
- Для кода: тесты, typecheck, линтер проекта (`pytest`, `pnpm test`, `pnpm typecheck`, `pnpm lint` — что есть в проекте)
|
- Для кода: тесты, typecheck, линтер проекта (`pytest`, `pnpm test`, `pnpm typecheck`, `pnpm lint` — что есть в проекте)
|
||||||
- Для инфраструктуры: `systemctl is-active`, healthcheck, статус деплоя
|
- Для инфраструктуры: `systemctl is-active`, healthcheck, статус деплоя
|
||||||
|
|||||||
+17
-9
@@ -2,16 +2,24 @@
|
|||||||
description: Исследователь и консультант по кодовой базе. Изучает архитектуру,
|
description: Исследователь и консультант по кодовой базе. Изучает архитектуру,
|
||||||
находит связи, объясняет устройство проекта. Read-only.
|
находит связи, объясняет устройство проекта. Read-only.
|
||||||
mode: all
|
mode: all
|
||||||
temperature: 0.2
|
|
||||||
color: "#4ecdc4"
|
color: "#4ecdc4"
|
||||||
permission:
|
steps: 50
|
||||||
bash:
|
request:
|
||||||
"*": ask
|
body:
|
||||||
ls *: allow
|
temperature: 0.2
|
||||||
webfetch: deny
|
permissions:
|
||||||
websearch: deny
|
- action: shell
|
||||||
external_directory: ask
|
resource: "*"
|
||||||
steps: 25
|
effect: allow
|
||||||
|
- action: webfetch
|
||||||
|
resource: "*"
|
||||||
|
effect: deny
|
||||||
|
- action: websearch
|
||||||
|
resource: "*"
|
||||||
|
effect: deny
|
||||||
|
- action: external_directory
|
||||||
|
resource: "*"
|
||||||
|
effect: ask
|
||||||
---
|
---
|
||||||
|
|
||||||
# Researcher — исследователь и консультант
|
# 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/`. Ничего за его пределами.
|
||||||
|
- Никаких установок новых инструментов и зависимостей без явного разрешения; не хватает
|
||||||
|
инструмента — скажи об этом.
|
||||||
|
- Эксплуатацию проводи только против подконтрольных инстансов и данных.
|
||||||
+12
-4
@@ -2,13 +2,21 @@
|
|||||||
description: Тестировщик, QA-инженер. Находит баги, проверяет фиксы, запускает
|
description: Тестировщик, QA-инженер. Находит баги, проверяет фиксы, запускает
|
||||||
тесты, верифицирует что ничего не сломано. Read-only.
|
тесты, верифицирует что ничего не сломано. Read-only.
|
||||||
mode: all
|
mode: all
|
||||||
|
request:
|
||||||
|
body:
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
steps: 50
|
steps: 50
|
||||||
color: "#ff6b6b"
|
color: "#ff6b6b"
|
||||||
permission:
|
permissions:
|
||||||
bash: allow
|
- action: shell
|
||||||
webfetch: deny
|
resource: "*"
|
||||||
websearch: deny
|
effect: allow
|
||||||
|
- action: webfetch
|
||||||
|
resource: "*"
|
||||||
|
effect: deny
|
||||||
|
- action: websearch
|
||||||
|
resource: "*"
|
||||||
|
effect: deny
|
||||||
---
|
---
|
||||||
|
|
||||||
# Тестировщик, QA-инженер
|
# Тестировщик, 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