commit e76236dc55de762d806a6d38a923aeb3d7951cf3 Author: Yuriy Date: Sat Jan 31 11:33:19 2026 +0300 Initial commit: papa-yu v2.4.4 - Schema version (x_schema_version, schema_hash) в prompt/trace - Кеш read/search/logs/env (ContextCache) в plan-цикле - Контекст-диета: MAX_FILES=8, MAX_FILE_CHARS=20k, MAX_TOTAL_CHARS=120k - Plan→Apply двухфазность, NO_CHANGES, path sanitization - Protected paths, content validation, EOL normalization - Trace (PAPAYU_TRACE), redaction (PAPAYU_TRACE_RAW) - Preview diff, undo/redo, transactional apply Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13ddc3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Секреты — не коммитить +.env +.env.* +!.env.example +*.key +*.pem + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Vite / Build +dist/ +*.local + +# Rust / Tauri +src-tauri/target/ +**/*.rs.bk +Cargo.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# TypeScript cache +*.tsbuildinfo +tsconfig.tsbuildinfo +tsconfig.node.tsbuildinfo + +# Временные файлы +*.tmp +*.temp diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..47d6fa5 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,108 @@ +# Полный аудит приложения PAPA YU + +**Дата:** 2026-01-28 +**Цель:** проверка компонентов, связей UI ↔ backend, исправление отображения блока выбора пути к папкам и заключение. + +--- + +## 1. Структура проекта + +| Путь | Назначение | +|------|------------| +| `src/` | React (Vite) — UI | +| `src/pages/Tasks.tsx` | Страница «Задачи» — анализ проекта, выбор папок, preview/apply/undo/redo | +| `src/pages/Dashboard.tsx` | Страница «Панель управления» | +| `src/App.tsx` | Роутинг, Layout (header + main) | +| `src-tauri/src/` | Rust (Tauri) — команды, tx, types | + +--- + +## 2. Проверенные компоненты + +### 2.1 UI + +- **App.tsx** — маршруты `/` (Tasks), `/control-panel` (Dashboard). Layout: header с навигацией, main с `overflow: visible` (исправлено). +- **Tasks.tsx** — блок «Путь к папке проекта»: + - Расположен **первым** под заголовком «Анализ проекта». + - Секция с `data-section="path-selection"` и классом `tasks-sources`. + - Две кнопки: **«Выбрать папку»** (основная синяя), **«+ Добавить ещё папку»**. + - Список выбранных папок или текст «Папки не выбраны. Нажмите кнопку «Выбрать папку» выше.». + - Ниже: поле ввода пути и кнопка «Отправить». +- **index.css** — правило для `.tasks-sources[data-section="path-selection"]`: `display: block !important`, `visibility: visible !important`, чтобы блок не скрывался. + +### 2.2 Связи UI → Backend (invoke) + +| Действие в UI | Команда Tauri | Файл Rust | +|---------------|---------------|-----------| +| Загрузка списка папок при монтировании | `get_folder_links` | folder_links.rs | +| Сохранение списка папок | `set_folder_links` (links: { paths }) | folder_links.rs | +| Анализ + preview + apply (пакет) | `run_batch_cmd` (payload: paths, confirm_apply, auto_check, selected_actions) | run_batch.rs | +| Состояние undo/redo | `get_undo_redo_state_cmd` | undo_last.rs, tx/store.rs | +| Откат | `undo_last` | undo_last.rs | +| Повтор | `redo_last` | redo_last.rs | +| Генерация плана (v2.4) | `generate_actions` | generate_actions.rs | + +Выбор папки через диалог: `open({ directory: true })` из `@tauri-apps/plugin-dialog` — плагин зарегистрирован в `lib.rs` (`tauri_plugin_dialog::init()`). + +--- + +## 3. Backend (Rust) + +### 3.1 Зарегистрированные команды (lib.rs) + +- `analyze_project_cmd`, `preview_actions_cmd`, `apply_actions_cmd`, `run_batch_cmd` +- `undo_last`, `undo_available`, `redo_last`, `get_undo_redo_state_cmd` +- `generate_actions` +- `get_folder_links`, `set_folder_links` + +### 3.2 Модули + +- **commands/** — analyze_project, apply_actions, preview_actions, run_batch, undo_last, redo_last, generate_actions, folder_links, auto_check. +- **tx/** — limits (preflight), store (undo/redo stacks), mod (snapshot_before, rollback_tx, apply_actions_to_disk, collect_rel_paths, write_manifest, read_manifest, etc.). +- **types** — ApplyPayload, ApplyResult, TxManifest, Action, ActionKind, AnalyzeReport, BatchPayload, BatchEvent, etc. + +### 3.3 Folder links + +- `FolderLinks { paths: Vec }` — сериализуется в `app_data_dir/folder_links.json`. +- `load_folder_links`, `save_folder_links` — используются в `get_folder_links` / `set_folder_links`. + +Связь с UI: при загрузке Tasks вызывается `get_folder_links` и при необходимости обновляется `folderLinks`; при добавлении/удалении папки вызывается `set_folder_links`. Формат `{ links: { paths } }` соответствует типу `FolderLinks`. + +--- + +## 4. Внесённые исправления + +1. **Блок выбора пути к папке (Tasks.tsx)** + - Секция «Путь к папке проекта» вынесена в начало страницы (сразу под заголовком). + - Заголовок секции: «Путь к папке проекта», подпись с указанием нажать кнопку или ввести путь. + - Кнопки «Выбрать папку» и «+ Добавить ещё папку» оформлены заметно (размер, контраст, тень у основной). + - Добавлены `className="tasks-sources"` и `data-section="path-selection"` для стилей и отладки. + - Секция с рамкой, фоном и `minHeight: 140px`, чтобы блок всегда занимал место и был виден. + - В строке ввода оставлены только поле пути и «Отправить» (дублирующая кнопка «Выбрать папку» убрана, чтобы не путать с блоком выше). + +2. **Layout (App.tsx)** + - Для `main` заданы `overflow: visible` и `minHeight: 0`, чтобы контент не обрезался. + +3. **Глобальные стили (index.css)** + - Добавлено правило для `.tasks-sources[data-section="path-selection"]`: блок принудительно видим. + +--- + +## 5. Рекомендации после обновления кода + +- Перезапустить приложение: `cd papa-yu/src-tauri && cargo tauri dev`. +- В браузере/WebView сделать жёсткое обновление (Ctrl+Shift+R / Cmd+Shift+R), чтобы подтянуть новый UI без кэша. +- Если используется только фронт (Vite): перезапустить `npm run dev` и обновить страницу. + +После этого в начале страницы «Задачи» должен отображаться блок «Путь к папке проекта» с кнопками «Выбрать папку» и «+ Добавить ещё папку» и списком выбранных папок. + +--- + +## 6. Заключение + +- **Компоненты:** App, Tasks, Dashboard, Layout и точки входа (main.tsx, index.html) проверены; маршруты и вложенность корректны. +- **Связи UI ↔ backend:** вызовы `get_folder_links`, `set_folder_links`, `run_batch_cmd`, `get_undo_redo_state_cmd`, `undo_last`, `redo_last` соответствуют зарегистрированным командам и типам (FolderLinks, BatchPayload, ApplyPayload и т.д.). +- **Исправления:** блок выбора пути к папкам сделан первым и визуально выделен; добавлены гарантии видимости через разметку и CSS; дублирование кнопки убрано. +- **Ошибки:** явных ошибок в компонентах и связях не выявлено. Если на экране по-прежнему не видно кнопок и блока, наиболее вероятны кэш сборки или WebView — выполнить перезапуск и жёсткое обновление по п. 5. + +Аудит выполнен. Состояние: **исправления внесены, рекомендации по обновлению даны.** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..684afbf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +Все значимые изменения в проекте PAPA YU фиксируются в этом файле. + +Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/). + +--- + +## [2.4.4] — 2025-01-31 + +### Добавлено + +- **UX:** история сессий по проекту — блок «История сессий» с раскрывающимся списком сессий (дата, количество событий, последнее сообщение); обновление списка после agentic run. +- **UX:** в блоке профиля отображаются лимиты (max_actions_per_tx, timeout_sec). +- **UX:** фильтр расширений в диалоге «Прикрепить файл» (исходники и конфиги: .ts, .tsx, .js, .jsx, .rs, .py, .json, .toml, .md, .yml, .yaml, .css, .html, .xml). +- **UX:** горячие клавиши — Ctrl+Enter (Cmd+Enter): отправить/запустить анализ; Escape: сбросить превью изменений. +- **UX:** тёмная тема — переключатель в боковой панели, CSS-переменные для обоих режимов, сохранение выбора в localStorage, поддержка системных настроек. +- **UX:** экспорт/импорт настроек — кнопки в боковой панели для сохранения и восстановления всех настроек (проекты, профили, сессии, папки) в JSON-файл. +- **Тестирование:** юнит-тесты в Rust для `detect_project_type`, `get_project_limits`, `is_protected_file`, `is_text_allowed`, `settings_export` (18 тестов). +- **Тестирование:** тестовые фикстуры в `tests/fixtures/` — минимальные проекты для E2E тестирования (minimal-node, minimal-rust). +- **Документация:** E2E сценарий в `docs/E2E_SCENARIO.md`; обновлён README до v2.4.4; README для тестов в `tests/README.md`. +- **Контекст прикреплённых файлов:** в отчёт и batch передаётся список прикреплённых файлов (`attached_files` в `BatchPayload` и `AnalyzeReport`); фронт передаёт его при вызове `runBatchCmd`. +- **LLM-планировщик:** при заданном `PAPAYU_LLM_API_URL` команда «Предложить исправления» вызывает OpenAI-совместимый API (OpenAI, Ollama и др.); ответ парсится в план действий (CREATE_FILE, CREATE_DIR и т.д.). Без настройки — эвристический план по отчёту. +- **Бэкенд:** команды `export_settings` и `import_settings` для резервного копирования и переноса настроек между машинами. +- **Конфиг:** расширенный allowlist команд verify (`verify_allowlist.json`) — добавлены cargo clippy, tsc --noEmit, mypy, pytest --collect-only. +- **Инфраструктура:** инициализирован Git-репозиторий с улучшенным .gitignore. +- **Preview diff в propose flow:** после получения плана автоматически вызывается `preview_actions`, diffs отображаются в UI. +- **ERR_UPDATE_WITHOUT_BASE:** в режиме APPLY UPDATE_FILE разрешён только для файлов, прочитанных в Plan (FILE[path] или === path ===). +- **Protected paths:** denylist для `.env`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`, `**/secrets/**`. +- **Content validation:** запрет NUL, >10% non-printable = ERR_PSEUDO_BINARY; лимиты max_path_len=240, max_actions=200, max_total_content_bytes=5MB. +- **EOL:** `PAPAYU_NORMALIZE_EOL=lf` — нормализация \r\n→\n и trailing newline. +- **Наблюдаемость:** trace_id (UUID) на каждый propose; лог-ивенты LLM_REQUEST_SENT, LLM_RESPONSE_OK, VALIDATION_FAILED, APPLY_SUCCESS, APPLY_ROLLBACK, PREVIEW_READY. +- **Трассировка:** `PAPAYU_TRACE=1` — запись в `.papa-yu/traces/.json`. +- **Детерминизм LLM:** temperature=0, max_tokens=65536, top_p=1, presence_penalty=0, frequency_penalty=0 (PAPAYU_LLM_TEMPERATURE, PAPAYU_LLM_MAX_TOKENS). +- **Capability detection:** при ошибке API response_format — автоматический retry без response_format (Ollama и др.). +- **Schema version:** `x_schema_version` в llm_response_schema.json; schema_hash (sha256) в trace; LLM_PLAN_SCHEMA_VERSION в prompt. +- **Кеш контекста:** read_file/search/logs/env кешируются в plan-цикле; CONTEXT_CACHE_HIT/MISS. +- **Контекст-диета:** PAPAYU_CONTEXT_MAX_FILES=8, MAX_FILE_CHARS=20k, MAX_TOTAL_CHARS=120k; head+tail truncation; CONTEXT_DIET_APPLIED. + +### Изменено + +- Лимиты профиля применяются в `apply_actions_tx` и `run_batch` — при превышении `max_actions_per_tx` возвращается ошибка TOO_MANY_ACTIONS. +- Таймаут проверок в verify и auto_check задаётся из профиля (`timeout_sec`); в `verify_project` добавлен таймаут на выполнение каждой проверки (spawn + try_wait + kill при превышении). +- Синхронизированы версии в package.json, Cargo.toml и tauri.conf.json. + +--- + +## [2.4.3] — ранее + +### Реализовано + +- Профиль по пути (тип проекта, лимиты, goal_template). +- Agentic run — цикл анализ → план → превью → применение → проверка → откат при ошибке. +- Прикрепление файлов, кнопка «Прикрепить файл». +- Guard опасных изменений (is_protected_file, is_text_allowed). +- Подтверждение Apply (user_confirmed). +- Единый API-слой (src/lib/tauri.ts), типы в src/lib/types.ts. +- Компоненты PathSelector, AgenticResult, хук useUndoRedo. +- Транзакционное apply с snapshot и откатом при падении auto_check. +- Undo/Redo по последней транзакции. +- Единый batch endpoint (run_batch): analyze → preview → apply (при confirmApply) → autoCheck. + +--- + +## [2.3.2] — ранее + +- Apply + Real Undo (snapshot в userData/history, откат при падении check). +- AutoCheck для Node, Rust, Python. +- Actions: README, .gitignore, tests/, .env.example. +- UX: двухфазное применение, кнопки «Показать исправления», «Применить», «Отмена», «Откатить последнее». +- Folder Links (localStorage + userData/folder_links.json). +- Брендинг PAPA YU, минимальный размер окна 1024×720. diff --git a/PAPA YU — Сборка и запуск.command b/PAPA YU — Сборка и запуск.command new file mode 100755 index 0000000..8dbe7fa --- /dev/null +++ b/PAPA YU — Сборка и запуск.command @@ -0,0 +1,17 @@ +#!/bin/bash +# PAPA YU — сборка приложения и запуск (первая установка или после обновления кода). +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +echo " Сборка PAPA YU..." +export CI=false +if ! npm run tauri build; then + echo "" + echo " Ошибка сборки. Проверьте: npm install, Rust и Xcode Command Line Tools." + read -n 1 -s -r -p " Нажмите любую клавишу..." + exit 1 +fi + +BUNDLE="$SCRIPT_DIR/src-tauri/target/release/bundle/macos/PAPA YU.app" +[ -d "$BUNDLE" ] && open "$BUNDLE" || open "$SCRIPT_DIR/src-tauri/target/release/bundle/macos" +echo " Готово." diff --git a/PAPA YU.command b/PAPA YU.command new file mode 100755 index 0000000..5cbe4d3 --- /dev/null +++ b/PAPA YU.command @@ -0,0 +1,25 @@ +#!/bin/bash +# PAPA YU — запуск приложения (двойной клик). Сборка не выполняется. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUNDLE_DIR="$SCRIPT_DIR/src-tauri/target/release/bundle/macos" + +find_app() { + [ -d "$BUNDLE_DIR/PAPA YU.app" ] && echo "$BUNDLE_DIR/PAPA YU.app" && return + for f in "$BUNDLE_DIR"/*.app; do + [ -d "$f" ] && echo "$f" && return + done + echo "" +} + +APP=$(find_app) +if [ -n "$APP" ]; then + open "$APP" + exit 0 +fi + +echo "" +echo " PAPA YU не найден." +echo " Для первой сборки запустите: «PAPA YU — Сборка и запуск.command»" +echo "" +read -n 1 -s -r -p " Нажмите любую клавишу..." +exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..77ed3dc --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# PAPA YU v2.4.4 + +Десктопное приложение для анализа проекта и автоматических исправлений (README, .gitignore, tests/, структура) с **транзакционным apply**, **реальным undo** и **autoCheck с откатом**. + +## Единственная папка проекта + +Вся разработка, сборка и запуск ведутся из **этой папки** (например `/Users/.../Desktop/papa-yu`). ТЗ и спецификации лежат отдельно в папке **папа-ю** на рабочем столе (не переносятся). Подробнее: `docs/ЕДИНАЯ_ПАПКА_ПРОЕКТА.md`. + +**Запуск без терминала:** двойной клик по `PAPA YU.command` (только запуск) или по `PAPA YU — Сборка и запуск.command` (сборка + запуск). + +## Требования + +- Node.js 18+ +- Rust 1.70+ +- npm + +## Запуск + +```bash +cd papa-yu +npm install +npm run tauri dev +``` + +Из корня проекта можно также: `cd src-tauri && cargo tauri dev`. + +**Если в окне видно «Could not fetch a valid…»** — фронт не загрузился. Запускайте приложение только так: в терминале из папки проекта выполните `npm run tauri dev` (это поднимает и Vite, и Tauri). Не открывайте скомпилированный .app без dev-сервера, если хотите видеть интерфейс. + +## Сборка + +```bash +npm run tauri build +``` + +## v2.4.4 — что реализовано + +### Анализ и профиль + +- **Анализ по пути** — выбор папки или ввод пути вручную; анализ возвращает отчёт (findings, recommendations, actions, action_groups, fix_packs). +- **Профиль по пути** — автоматическое определение типа проекта (React/Vite, Next.js, Node, Rust, Python) и лимитов (max_actions_per_tx, timeout_sec, max_files). Профиль и лимиты отображаются в форме. + +### Применение и откат + +- **Транзакционное apply** — перед применением создаётся снимок; после apply выполняется autoCheck (cargo check / npm run build и т.д.) с таймаутом из профиля. При падении проверки — автоматический откат. +- **Лимиты профиля** — в `apply_actions_tx` и `run_batch` проверяется число действий против `max_actions_per_tx`; при превышении возвращается ошибка TOO_MANY_ACTIONS. Таймаут проверок задаётся из профиля. +- **Undo/Redo** — откат последней транзакции и повтор; состояние отображается в UI. + +### Безопасность + +- **Защита путей** — запрещено изменение служебных путей (.git, node_modules, target, dist и т.д.) и бинарных файлов; разрешены только текстовые расширения (см. guard в коде). +- **Подтверждение** — применение только при явном подтверждении пользователя (user_confirmed). +- **Allowlist команд** — в verify и auto_check выполняются только разрешённые команды с фиксированными аргументами (конфиг в `src-tauri/config/verify_allowlist.json`). + +### UX + +- **Папки и файлы** — выбор папки, прикрепление файлов (с фильтром расширений: .ts, .tsx, .rs, .py, .json, .toml и др.), ручной ввод пути. +- **История сессий** — по выбранному проекту отображается список сессий (дата, количество событий); после agentic run список обновляется. +- **Горячие клавиши** — Ctrl+Enter (Cmd+Enter на Mac): отправить/запустить анализ; Escape: сбросить превью изменений. +- **Тёмная тема** — переключатель в боковой панели; выбор сохраняется в localStorage; поддержка системных настроек темы. +- **Экспорт/импорт настроек** — кнопки «Экспорт» и «Импорт» в боковой панели для сохранения и восстановления всех настроек (проекты, профили, сессии, папки) в JSON-файл. + +### Режимы + +- **Batch** — анализ → превью → при необходимости применение с проверками (одна команда `run_batch`). +- **Исправить автоматически (agentic run)** — цикл: анализ → план → превью → применение → проверка; при неудаче проверки — откат и повтор в пределах max_attempts. +- **Безопасные исправления в один клик** — генерация безопасных действий по отчёту → превью → применение с проверкой. +- **Предложить исправления** — план по отчёту и цели: при наличии настройки LLM — вызов внешнего API (OpenAI/Ollama и др.), иначе эвристика. + +### LLM-планировщик (опционально) + +Для кнопки «Предложить исправления» можно включить генерацию плана через OpenAI-совместимый API. Задайте переменные окружения перед запуском приложения: + +- **`PAPAYU_LLM_API_URL`** — URL API (обязательно), например: + - OpenAI: `https://api.openai.com/v1/chat/completions` + - Ollama (локально): `http://localhost:11434/v1/chat/completions` +- **`PAPAYU_LLM_API_KEY`** — API-ключ (для OpenAI и облачных API; для Ollama можно не задавать). +- **`PAPAYU_LLM_MODEL`** — модель (по умолчанию `gpt-4o-mini`), для Ollama — например `llama3.2`. +- **`PAPAYU_LLM_STRICT_JSON`** — при `1`/`true` добавляет `response_format: { type: "json_schema", ... }` (OpenAI Structured Outputs; Ollama может не поддерживать). + +**Поведение strict / best-effort:** +- Если strict включён: приложение отправляет `response_format` в API; при невалидном ответе — локальная валидация схемы отклоняет и выполняется **1 авто-ретрай** с repair-подсказкой («Верни ТОЛЬКО валидный JSON…»). +- Если strict выключен или провайдер не поддерживает: **best-effort** парсинг (извлечение из markdown), затем локальная валидация схемы; при неудаче — тот же repair-ретрай. + +Пример для Ollama (без ключа, локально): + +```bash +export PAPAYU_LLM_API_URL="http://localhost:11434/v1/chat/completions" +export PAPAYU_LLM_MODEL="llama3.2" +npm run tauri dev +``` + +После этого кнопка «Предложить исправления» будет строить план через выбранный LLM. + +Если `PAPAYU_LLM_API_URL` не задан или пуст, используется встроенная эвристика (README, .gitignore, LICENSE, .env.example по правилам). + +### Тестирование + +- **Юнит-тесты (Rust)** — тесты для `detect_project_type`, `get_project_limits`, `is_protected_file`, `is_text_allowed` (см. `src-tauri/src/commands/get_project_profile.rs` и `apply_actions_tx.rs`). Запуск: `cd src-tauri && cargo test`. +- **E2E сценарий** — описание сценария «анализ → применение → undo» и критерии успеха см. в `docs/E2E_SCENARIO.md`. + +### Архитектура + +- **Фронт:** React 18, Vite 5, TypeScript; типы в `src/lib/types.ts`, единый API-слой в `src/lib/tauri.ts`; компоненты PathSelector, AgenticResult, хук useUndoRedo. +- **Бэкенд:** Tauri 2, Rust; команды в `src-tauri/src/commands/`, транзакции и undo/redo в `tx/`, verify с таймаутом в `verify.rs`. + +## Документация + +- `docs/IMPROVEMENTS.md` — рекомендации по улучшениям. +- `docs/E2E_SCENARIO.md` — E2E сценарий и критерии успеха. +- `docs/ОЦЕНКА_И_СЦЕНАРИЙ_РАССКАЗА.md` — оценка необходимости обновлений и сценарий рассказа о программе по модулям. +- `CHANGELOG.md` — история изменений по версиям. diff --git a/docs/AGENT_CONTRACT.md b/docs/AGENT_CONTRACT.md new file mode 100644 index 0000000..452ed5b --- /dev/null +++ b/docs/AGENT_CONTRACT.md @@ -0,0 +1,70 @@ +# Контракт поведения агента (оркестратор) + +Это не prompt, а логика приложения: когда агент должен спрашивать, когда действовать, что запрещено. + +--- + +## Когда агент должен спрашивать + +- Нет языка/версии/runtime (неясно, Python 3.11 или Node 18). +- Отсутствуют логи/stacktrace (пользователь написал «падает», но не приложил вывод). +- Не ясно, «исправить» или «объяснить» (нужно уточнить намерение). +- Конфликт требований (скорость vs читаемость vs безопасность) — предложить варианты. + +--- + +## Когда агент должен действовать сразу + +- Есть stacktrace + доступ к коду (файлы в контексте). +- Есть конкретный файл/функция в запросе. +- Просьба однозначна: «написать тест», «рефактор этого блока», «добавь README». + +--- + +## Запреты (оркестратор должен проверять) + +- **Нельзя** писать «тесты прошли», если инструмент `run_tests` не вызывался и результат не передан. +- **Нельзя** ссылаться на «файл X» или «строка N», если инструмент `read_file` не вызывался и содержимое не в контексте. +- **Нельзя** утверждать, что команда/сборка выполнена, если `run` не вызывался и вывода нет. + +--- + +## Режимы + +| Режим | Назначение | +|--------|------------| +| **Chat** | Инженер-коллега: обсуждение, уточнения, план; ответы точные и проверяемые. | +| **Fix-it** | Обязан вернуть: диагноз (1–3 пункта), patch/diff, команды проверки, риски при наличии. | + +Режим задаётся переменной окружения `PAPAYU_LLM_MODE=chat` (по умолчанию) или `PAPAYU_LLM_MODE=fixit`. + +--- + +## Связь с Tools (function calling) + +При использовании OpenAI-совместимого API с tool calling оркестратор должен: + +1. Выполнять вызовы инструментов (list_files, read_file, search_in_repo, run_tests, apply_patch и т.д.) в приложении. +2. Передавать результаты обратно в модель (tool output с привязкой к call_id). +3. Не считать задачу «выполненной» (тесты прошли, патч применён), пока соответствующий инструмент не вернул успех и результат не передан агенту. + +Схема tools: см. `docs/openai_tools_schema.json`. + +--- + +## Стиль ответа (опционально) + +- **verbosity: 0..2** — 0 ультракоротко, 2 с объяснениями (runtime-настройка). +- **ask_budget: 0..2** — сколько уточняющих вопросов допустимо за один оборот. +- Формат ответа по умолчанию: 3–7 буллетов; код/патч — отдельным блоком; в конце: «Что сделать сейчас: …». + +--- + +## Безопасность apply_patch + +При реализации инструмента `apply_patch`: + +- dry-run валидация diff перед применением; +- запрет изменений вне repo-root; +- лимит размера патча; +- обязательный backup/undo (например, через tx в papa-yu). diff --git a/docs/E2E_SCENARIO.md b/docs/E2E_SCENARIO.md new file mode 100644 index 0000000..45056ef --- /dev/null +++ b/docs/E2E_SCENARIO.md @@ -0,0 +1,38 @@ +# E2E сценарий: анализ → применение → undo + +Сценарий для ручной или автоматической проверки полного цикла работы приложения. + +## Предусловия + +- Установлены зависимости: `npm install`, `cargo build` (или `npm run tauri build`). +- Приложение запущено: `npm run tauri dev`. +- Есть тестовая папка с минимальным проектом (например, пустая папка с `package.json` или `Cargo.toml` без README). + +## Шаги + +1. **Выбор папки** + Нажать «Выбрать папку» и указать путь к тестовой папке (или ввести путь вручную и нажать «Отправить» / Ctrl+Enter). + +2. **Анализ** + Убедиться, что запустился анализ и в ленте появилось сообщение ассистента вида «Нашёл X проблем. Могу исправить Y.» и отчёт (findings, actions). + +3. **Применение** + Нажать «Применить рекомендованные исправления» или выбрать действия и нажать «Предпросмотр изменений», затем «Применить». Убедиться, что появилось сообщение об успешном применении и при необходимости — о прохождении проверок (auto_check). + +4. **Откат (Undo)** + Нажать «Откатить» (или кнопку «Откатить изменения (Undo)» в блоке результата). Убедиться, что появилось сообщение «Последнее действие отменено.» или «Откат выполнен.» и что файлы на диске вернулись в состояние до применения (например, созданный README удалён). + +## Критерии успеха + +- Анализ возвращает отчёт с путём и списком действий. +- Применение создаёт/изменяет файлы и при включённом auto_check выполняет проверки (cargo check / npm run build и т.д.). +- После undo состояние проекта на диске соответствует состоянию до применения; кнопка «Откатить» снова неактивна (или активна для предыдущей транзакции). + +## Автоматизация (будущее) + +Для автоматического E2E можно использовать: + +- **Tauri test** — запуск приложения и вызов команд через контекст теста. +- **Playwright** — управление окном приложения (WebView) и клики по кнопкам, проверка текста в ленте. + +Текущие юнит-тесты эвристик (detect_project_type, is_protected_file, is_text_allowed) запускаются командой: `cd src-tauri && cargo test`. diff --git a/docs/FIX_PLAN_CONTRACT.md b/docs/FIX_PLAN_CONTRACT.md new file mode 100644 index 0000000..9bff202 --- /dev/null +++ b/docs/FIX_PLAN_CONTRACT.md @@ -0,0 +1,261 @@ +# Fix-plan оркестратор: контракты JSON и автосбор контекста + +papa-yu — **Rust/Tauri**, не Python. Ниже — текущий JSON-ответ, расширенный контракт Fix-plan/Apply и как это встроено в приложение. + +--- + +## 1) Текущий JSON-ответ (как есть сейчас) + +Модель возвращает **один** JSON. Приложение парсит и применяет действия по подтверждению пользователя. + +### Вариант A: массив действий + +```json +[ + { "kind": "CREATE_FILE", "path": "README.md", "content": "# Project\n" }, + { "kind": "CREATE_DIR", "path": "src" } +] +``` + +### Вариант B: объект с actions + memory_patch + +```json +{ + "actions": [ + { "kind": "UPDATE_FILE", "path": "src/main.py", "content": "..." } + ], + "memory_patch": { + "project.default_test_command": "pytest -q" + } +} +``` + +### Поля элемента actions + +| Поле | Тип | Обязательность | Описание | +|----------|--------|----------------|----------| +| `kind` | string | да | `CREATE_FILE` \| `CREATE_DIR` \| `UPDATE_FILE` \| `DELETE_FILE` \| `DELETE_DIR` | +| `path` | string | да | Относительный путь от корня проекта | +| `content`| string | нет | Для CREATE_FILE / UPDATE_FILE | + +### Результат в приложении + +- `AgentPlan`: `{ ok, summary, actions, error?, error_code? }` +- `memory_patch` применяется по whitelist и сохраняется в `preferences.json` / `.papa-yu/project.json` + +--- + +## 2) Расширенный контракт: Fix-plan и Apply + +Один JSON-объект с полем `mode`. Приложение понимает оба формата (текущий и расширенный). + +### Режим fix-plan (только план, применение после подтверждения) + +```json +{ + "mode": "fix-plan", + "summary": "Коротко: почему падает и что делаем", + "questions": ["Нужен ли тест на X?"], + "context_requests": [ + { "type": "read_file", "path": "src/x.py", "start_line": 1, "end_line": 220 }, + { "type": "search", "query": "SomeSymbol", "glob": "**/*.py" }, + { "type": "logs", "source": "runtime", "last_n": 200 } + ], + "plan": [ + { "step": "Диагностика", "details": "..." }, + { "step": "Правка", "details": "..." }, + { "step": "Проверка", "details": "Запустить pytest -q" } + ], + "proposed_changes": { + "patch": "unified diff (optional в fix-plan)", + "actions": [ + { "kind": "UPDATE_FILE", "path": "src/x.py", "content": "..." } + ], + "commands_to_run": ["pytest -q"] + }, + "risks": ["Затрагивает миграции"], + "memory_patch": { + "project.default_test_command": "pytest -q" + } +} +``` + +- Если есть `context_requests`, приложение подтягивает контекст (read_file, search, logs) и повторяет запрос к модели (до 2 раундов). +- Действия для UI/apply берутся из `proposed_changes.actions` (если есть), иначе из корневого `actions` (обратная совместимость). + +### Режим apply (после «ок» пользователя) + +```json +{ + "mode": "apply", + "summary": "Что применяем", + "patch": "unified diff (обязательно при применении diff)", + "commands_to_run": ["pytest -q", "ruff check ."], + "verification": ["Ожидаем: все тесты зелёные"], + "rollback": ["git checkout -- "], + "memory_patch": {} +} +``` + +В текущей реализации применение идёт по списку **actions** (CREATE_FILE/UPDATE_FILE/…). Поле `patch` (unified diff) зарезервировано под будущую поддержку `apply_patch` в бэкенде. + +--- + +## 3) System prompt под один JSON (Fix-plan) + +Ядро, которое вставляется при режиме Fix-plan (переменная `PAPAYU_LLM_MODE=fix-plan` или отдельный промпт): + +```text +Ты — инженерный ассистент внутри программы для создания, анализа и исправления кода. Оператор один: я. +Всегда отвечай ОДНИМ валидным JSON-объектом. Никакого текста вне JSON. + +Режимы: +- "fix-plan": предлагаешь план и (опционально) proposed_changes (actions, patch, commands_to_run). Ничего не применяешь. +- "apply": выдаёшь финальный patch и команды для применения/проверки (после подтверждения оператора). + +Правила: +- Не выдумывай содержимое файлов/логов. Если нужно — запроси через context_requests. +- Никогда не утверждай, что тесты/команды запускались, если их не запускало приложение. +- Если данных не хватает — задай максимум 2 вопроса в questions и/или добавь context_requests. +- Минимальные изменения. Без широких рефакторингов без явного запроса. + +ENGINEERING_MEMORY: +{...вставляется приложением...} + +Схема JSON: +- mode: "fix-plan" | "apply" (или опусти для обратной совместимости — тогда ожидается массив actions или объект с actions) +- summary: string +- questions: string[] +- context_requests: [{ type: "read_file"|"search"|"logs"|"env", path?, start_line?, end_line?, query?, glob?, source?, last_n? }] +- plan: [{ step, details }] +- proposed_changes: { patch?, actions?, commands_to_run? } +- patch: string (обязательно в apply при применении diff) +- commands_to_run: string[] +- verification: string[] +- risks: string[] +- rollback: string[] +- memory_patch: object (только ключи из whitelist) +``` + +--- + +## 4) Автосбор контекста (без tools) + +Приложение **до** первого запроса к модели собирает базовый контекст и подставляет в user-сообщение: + +### Базовый набор + +- **env**: версия Python/Node/OS, venv, менеджер зависимостей (если определимо по проекту). +- **project prefs**: команды тестов/линта/формата из `.papa-yu/project.json` (уже в ENGINEERING_MEMORY). +- **recent_files**: список недавно открытых/изменённых файлов из `report_json` (если передан). +- **logs**: последние N строк логов (runtime/build) — если приложение имеет к ним доступ. + +### При ошибке/stacktrace в запросе + +- Распарсить пути и номера строк из Traceback. +- Добавить в контекст фрагменты файлов ±80 строк вокруг указанных строк. +- При «падает тест X» — подтянуть файл теста и (по возможности) тестируемый модуль. + +Эвристики реализованы в Rust: см. `context::gather_base_context`, `context::fulfill_context_requests`. + +--- + +## 5) JSON Schema для response_format (OpenAI Chat Completions) + +Используется endpoint **Chat Completions** (`/v1/chat/completions`). Для строгого JSON можно передать `response_format: { type: "json_schema", json_schema: { ... } }` (если провайдер поддерживает). + +Пример схемы под объединённый контракт (и текущий, и Fix-plan): + +```json +{ + "name": "papa_yu_plan_response", + "strict": true, + "schema": { + "type": "object", + "properties": { + "mode": { "type": "string", "enum": ["fix-plan", "apply"] }, + "summary": { "type": "string" }, + "questions": { "type": "array", "items": { "type": "string" } }, + "context_requests": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["read_file", "search", "logs", "env"] }, + "path": { "type": "string" }, + "start_line": { "type": "integer" }, + "end_line": { "type": "integer" }, + "query": { "type": "string" }, + "glob": { "type": "string" }, + "source": { "type": "string" }, + "last_n": { "type": "integer" } + } + } + }, + "plan": { + "type": "array", + "items": { "type": "object", "properties": { "step": { "type": "string" }, "details": { "type": "string" } } } + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { "type": "string", "enum": ["CREATE_FILE", "CREATE_DIR", "UPDATE_FILE", "DELETE_FILE", "DELETE_DIR"] }, + "path": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["kind", "path"] + } + }, + "proposed_changes": { + "type": "object", + "properties": { + "patch": { "type": "string" }, + "actions": { "type": "array", "items": { "$ref": "#/definitions/action" } }, + "commands_to_run": { "type": "array", "items": { "type": "string" } } + } + }, + "patch": { "type": "string" }, + "commands_to_run": { "type": "array", "items": { "type": "string" } }, + "verification": { "type": "array", "items": { "type": "string" } }, + "risks": { "type": "array", "items": { "type": "string" } }, + "rollback": { "type": "array", "items": { "type": "string" } }, + "memory_patch": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": true + } +} +``` + +Для «только массив actions» схему можно упростить или использовать два варианта (массив vs объект) на стороне парсера — текущий парсер в Rust принимает и массив, и объект с `actions` и `memory_patch`. + +--- + +## 6) Режим в приложении + +Переменная окружения **`PAPAYU_LLM_MODE`**: +- `chat` (по умолчанию) — инженер-коллега, ответ массив/объект с `actions`. +- `fixit` — обязан вернуть патч и проверку (текущий FIXIT prompt). +- **`fix-plan`** — один JSON с `mode`, `summary`, `context_requests`, `plan`, `proposed_changes`, `memory_patch`; автосбор контекста и до 2 раундов по `context_requests`. + +ENGINEERING_MEMORY подставляется в system prompt приложением (см. `memory::build_memory_block`). + +--- + +## 7) Как подключить в UI + +- **«Fix (plan)»** / текущий сценарий: вызов `propose_actions` → показ `summary`, `plan`, `risks`, `questions`, превью по `proposed_changes.actions` или `actions`. +- **«Применить»**: вызов `apply_actions_tx` с выбранными `actions` (из ответа модели). Память уже обновлена по `memory_patch` при парсинге ответа. + +Flow «сначала план → подтверждение → применение» обеспечивается тем, что приложение не применяет действия до явного подтверждения пользователя; модель может отдавать как короткий формат (массив/actions), так и расширенный (mode fix-plan + proposed_changes). + +--- + +## 8) Инженерная память + +- MEMORY BLOCK подставляется в system prompt. +- Модель заполняет `commands_to_run` из `project.default_test_command` и т.п. +- При явной просьбе «запомни …» модель возвращает `memory_patch`; приложение применяет его по whitelist и сохраняет в файлы. + +Whitelist и логика — в `src-tauri/src/memory.rs`. diff --git a/docs/IMPROVEMENTS.md b/docs/IMPROVEMENTS.md new file mode 100644 index 0000000..4912dca --- /dev/null +++ b/docs/IMPROVEMENTS.md @@ -0,0 +1,109 @@ +# Рекомендации по улучшению PAPAYU + +## 1. Архитектура и код + +- **Вынести типы UI в один модуль** + Сейчас интерфейсы (`Action`, `AnalyzeReport`, `AgenticRunResult` и т.д.) объявлены в `Tasks.tsx`. Имеет смысл вынести их в `src/types/` или `src/lib/types.ts` и импортировать в страницах — так проще переиспользовать и тестировать. + +- **Разбить Tasks.tsx** + Файл очень большой (1200+ строк). Имеет смысл вынести: + - блок выбора папок/файлов — в `components/PathSelector.tsx`; + - ленту сообщений — в `components/ChatFeed.tsx`; + - блок результата agentic run — в `components/AgenticResult.tsx`; + - хуки (`useAgenticRun`, `useBatch`, `useUndoRedo`) — в отдельные файлы в `hooks/`. + +- **Единый слой вызова бэкенда** + Сделать один модуль `src/lib/tauri.ts` (или `api.ts`) с функциями `analyze()`, `runBatch()`, `agenticRun()`, `getProjectProfile()` и т.д., которые внутри вызывают `invoke`. В компонентах использовать только этот слой — проще менять контракты и добавлять логирование/обработку ошибок. + +--- + +## 2. Безопасность и надёжность + +- **Жёсткие лимиты из профиля (v2.4.4)** + Сейчас `profile.limits.max_actions_per_tx` и `timeout_sec` используются в agentic_run, но не в `apply_actions_tx` и `run_batch`. Имеет смысл передавать лимиты в эти команды и на Rust отклонять запросы, превышающие `max_actions_per_tx`, и ограничивать время выполнения проверок (например, 60 с на `verify`). + +- **Таймаут в verify_project** + В `verify.rs` команды (`cargo check`, `npm run build` и т.д.) запускаются без таймаута. Стоит добавить таймаут (например, 60 с) через `std::process::Command` + отдельный поток или `tokio::process` с `kill_on_drop`, чтобы зависшая сборка не блокировала приложение. + +- **Расширить allowlist в verify** + Сейчас список разрешённых команд жёстко задан в коде. Имеет смысл вынести его в конфиг (например, в `tauri.conf.json` или отдельный JSON) и проверять только команды из allowlist с фиксированными аргументами. + +--- + +## 3. UX и интерфейс + +- **История сессий в UI** + На бэкенде уже есть `list_sessions`, `append_session_event`. В интерфейсе можно добавить боковую панель или выпадающий список «Последние сессии» по проекту и показывать историю сообщений/запусков. + +- **Использование профиля при запуске** + Показывать в форме выбранный тип проекта (React/Vite, Rust, Python и т.д.) и лимиты (max_attempts, max_actions). Для agentic run можно подставлять `goal_template` из профиля в поле «Цель» или показывать подсказку. + +- **Фильтр типов файлов при «Прикрепить файл»** + В диалоге выбора файлов задать `filters` (например, `.ts`, `.tsx`, `.rs`, `.py`, `.json`, `.toml`), чтобы по умолчанию предлагать только исходники и конфиги. + +- **Клавиатурные сокращения** + Например: Ctrl+Enter — отправить сообщение/запустить анализ, Escape — сбросить превью. + +--- + +## 4. Тестирование + +- **Юнит-тесты для эвристик** + Покрыть тестами `build_plan` в `agentic_run.rs`, `generate_actions_from_report`, `detect_project_type` и `is_protected_file` / `is_text_allowed` — на них легко писать тесты с временными директориями. + +- **Интеграционные тесты** + Один-два E2E сценария (например, через `tauri test` или Playwright): выбор папки → анализ → применение безопасных правок → проверка, что файлы созданы и undo работает. + +- **Тестовые фикстуры** + В `docs/` уже есть JSON для auto-rollback. Имеет смысл добавить фикстуры «минимальный проект» (папка с `package.json` или `Cargo.toml` без README) и использовать их в тестах и вручную. + +--- + +## 5. Документация и конфиг + +- **Обновить README** + Привести версию к 2.4.3, описать: профиль по пути, agentic run, прикрепление файлов, кнопку «Прикрепить файл», guard опасных изменений и подтверждение Apply. + +- **CHANGELOG** + Ведение краткого CHANGELOG (по версиям) упростит онбординг и откат изменений. + +- **Команда запуска в одном месте** + В README указать одну команду, например: `cd src-tauri && cargo tauri dev` (или `npm run tauri dev` из корня), чтобы не было расхождений. + +--- + +## 6. Следующие фичи (по приоритету) + +1. **v2.4.4 — Profile-driven limits** + Жёстко ограничивать в `apply_actions_tx` и `run_batch` число действий и таймаут проверок из `ProjectProfile`. + +2. **LLM-планировщик (v2.4.1)** *(реализовано в v2.4.4)* + В `propose_actions`: при заданном `PAPAYU_LLM_API_URL` вызывается OpenAI-совместимый API; ответ (JSON-массив действий) парсится в `AgentPlan`. Контур выполнения без изменений: preview → apply_tx → verify → rollback. Переменные: `PAPAYU_LLM_API_URL`, `PAPAYU_LLM_API_KEY`, `PAPAYU_LLM_MODEL`. См. README. + +3. **Контекст прикреплённых файлов** *(реализовано в v2.4.4)* + В бэкенд передаётся список прикреплённых файлов (`attached_files` в `BatchPayload` и `AnalyzeReport`); он пробрасывается в `run_batch` → `analyze_project`. В отчёте поле `attached_files` доступно для дальнейшего использования (например, пометка затронутых файлов в превью). + +4. **Экспорт/импорт настроек** + Сохранение и загрузка `ProjectSettings` (и при желании списка папок) в файл для переноса между машинами. + +5. **Тёмная тема** + Переменные CSS или контекст темы и переключатель в шапке. + +--- + +## 7. Краткий чек-лист + +- [x] Вынести типы и разбить `Tasks.tsx` на компоненты и хуки (v2.4.3) +- [x] Единый API-слой для `invoke` в `src/lib/` (v2.4.3) +- [x] Лимиты профиля в `apply_actions_tx` и `run_batch` + таймаут в `verify_project` (v2.4.4) +- [x] История сессий и отображение профиля в UI (v2.4.4) +- [x] Фильтр расширений в диалоге «Прикрепить файл» (v2.4.4) +- [x] Юнит-тесты для guard и эвристик (v2.4.4, 18 тестов) +- [x] Обновить README и добавить CHANGELOG (v2.4.4) +- [x] Контекст прикреплённых файлов (v2.4.4) +- [x] LLM-планировщик (v2.4.4, env: PAPAYU_LLM_API_URL / API_KEY / MODEL) +- [x] Тёмная тема с переключателем (v2.4.4) +- [x] Экспорт/импорт настроек (v2.4.4) +- [x] Расширенный allowlist команд verify в конфиге (v2.4.4) +- [x] Тестовые фикстуры для E2E (v2.4.4) +- [x] Git-репозиторий инициализирован (v2.4.4) diff --git a/docs/LLM_PLAN_FORMAT.md b/docs/LLM_PLAN_FORMAT.md new file mode 100644 index 0000000..16b20c5 --- /dev/null +++ b/docs/LLM_PLAN_FORMAT.md @@ -0,0 +1,271 @@ +# Стек papa-yu и JSON-контракт ответа (план) + +## На чём написан papa-yu + +| Слой | Стек | Примечание | +|-----------|---------------------|-------------------------------------| +| **Backend** | **Rust** (Tauri) | Команды, LLM, FS, apply, undo, tx | +| **Frontend** | **TypeScript + React** (Vite) | UI, запросы к Tauri (invoke) | + +Не Python/Node/Go — бэкенд полностью на Rust; фронт — React/Vite. + +--- + +## JSON Schema для response_format + +Полная схема для `response_format` (OpenAI Responses API и др.) — см. `docs/papa_yu_response_schema.json`. + +Схема для Chat Completions (`response_format: { type: "json_schema", ... }`) — `src-tauri/config/llm_response_schema.json`. Включается через `PAPAYU_LLM_STRICT_JSON=1`. + +**Поведение strict / best-effort:** +- **strict включён** — приложение отправляет `response_format` в API; при ответе, не проходящем JSON schema, локально отклоняет и выполняет 1 авто-ретрай с repair-подсказкой. +- **strict выключен или провайдер не поддерживает** — best-effort парсинг (извлечение из ```json ... ```), затем локальная валидация; при неудаче — тот же repair-ретрай. + +--- + +## Текущий JSON-контракт ответа (план от LLM) + +LLM должен вернуть **только валидный JSON** — либо массив действий, либо объект с полями. + +### Принимаемые форматы + +1. **Массив действий** — `[{ kind, path, content? }, ...]` +2. **Объект** — `{ actions?, proposed_changes.actions?, summary?, context_requests?, memory_patch? }` + +### Формат Action (элемент массива) + +| Поле | Тип | Обязательность | Описание | +|----------|--------|----------------|----------| +| `kind` | string | да | Один из: `CREATE_FILE`, `CREATE_DIR`, `UPDATE_FILE`, `DELETE_FILE`, `DELETE_DIR` | +| `path` | string | да | Относительный путь от корня проекта (без `../`, без абсолютных путей, без `~`) | +| `content`| string | да для CREATE_FILE, UPDATE_FILE | Содержимое файла; макс. ~1MB на файл; для `CREATE_DIR`/`DELETE_*` не используется | + +**Ограничения на path:** no `../`, no абсолютные (`/`, `C:\`), no `~`. Локальная валидация отклоняет. + +**DELETE_*:** требует подтверждения пользователя в UI (кнопка «Применить»). + +**Plan→Apply без кнопок:** +- Префиксы: `plan: <текст>` → режим Plan; `apply: <текст>` → режим Apply. +- Триггеры перехода: `ok`, `ок`, `apply`, `применяй`, `да` — при наличии сохранённого плана переключают на Apply. +- По умолчанию: «исправь/почини» → Plan; «создай/сгенерируй» → Apply. + +**APPLY без изменений (каноничный маркер):** +- Если изменений не требуется — верни `actions: []` и `summary`, **начинающийся с `NO_CHANGES:`** (строго). +- Пример: `"summary": "NO_CHANGES: Проверка завершена, правок не требуется."` + +**Конфликты действий:** +- Один path не должен иметь несовместимых действий: CREATE_FILE + UPDATE_FILE, DELETE + CREATE/UPDATE. +- Порядок применения: CREATE_DIR → CREATE_FILE/UPDATE_FILE → DELETE_FILE → DELETE_DIR. + +**Пути:** +- Запрещены: абсолютные (`/`, `//`), Windows drive (`C:/`), UNC (`//server/share`), `~`, сегменты `..` и `.`, пустой или только `.`. +- Лимиты: max_path_len=240, max_actions=200, max_total_content_bytes=5MB. + +**ERR_UPDATE_WITHOUT_BASE:** +- В режиме APPLY каждый UPDATE_FILE должен ссылаться на файл, прочитанный в Plan (FILE[path]: или === path === в plan_context). + +**Protected paths (denylist):** +- `.env`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`, `**/secrets/**` — запрещены для UPDATE/DELETE. + +**Content:** +- Запрещён NUL (`\0`), >10% non-printable = ERR_PSEUDO_BINARY. + +**EOL:** +- `PAPAYU_NORMALIZE_EOL=lf` — нормализовать \r\n→\n, trailing newline. + +**Наблюдаемость:** +- Каждый propose имеет `trace_id` (UUID). Лог-ивенты в stderr: `LLM_REQUEST_SENT` (token_budget, input_chars), `LLM_RESPONSE_OK`, `VALIDATION_FAILED`, `LLM_REQUEST_TIMEOUT`, `LLM_RESPONSE_FORMAT_FALLBACK`. +- `PAPAYU_TRACE=1` — трасса в `.papa-yu/traces/.json`. По умолчанию raw_content не сохраняется (риск секретов); `PAPAYU_TRACE_RAW=1` — сохранять с маскировкой sk-/Bearer. В трассе — `config_snapshot`. + +**Параметры генерации:** temperature=0, max_tokens=16384 (авто-кэп: при input>80k → 4096), top_p=1, presence_penalty=0, frequency_penalty=0. `PAPAYU_LLM_TIMEOUT_SEC=90`. Capability detection: при ошибке response_format — retry без него. + +**Версия схемы:** `LLM_PLAN_SCHEMA_VERSION=1` — в system prompt и trace; для будущей поддержки v1/v2 при расширении kinds/полей. `x_schema_version` в llm_response_schema.json. `schema_hash` (sha256) в config_snapshot. + +**Кеш контекста:** read_file/search/logs/env кешируются в пределах plan-цикла. Логи: CONTEXT_CACHE_HIT, CONTEXT_CACHE_MISS. + +**Контекст-диета:** PAPAYU_CONTEXT_MAX_FILES=8, PAPAYU_CONTEXT_MAX_FILE_CHARS=20000, PAPAYU_CONTEXT_MAX_TOTAL_CHARS=120000. Файлы: head+tail truncation. Лог: CONTEXT_DIET_APPLIED. + +### Fix-plan режим (user.output_format) + +- **PLAN** (`plan`): `actions` пустой массив `[]`, `summary` обязателен (диагноз + шаги + команды проверки), при необходимости — `context_requests`. +- **APPLY** (`apply`): `actions` непустой, если нужны изменения; иначе пустой + `summary` «изменений не требуется». `summary` — что сделано и как проверить (используй `project.default_test_command` если задан). + +### Пример ответа (объект) + +```json +{ + "actions": [ + { "kind": "CREATE_FILE", "path": "README.md", "content": "# Project\n\n## Run\n\n`make run`\n" }, + { "kind": "CREATE_DIR", "path": "src" } + ], + "summary": "Созданы README и папка src.", + "context_requests": [], + "memory_patch": {} +} +``` + +### Пример Fix-plan (plan-режим) + +```json +{ + "actions": [], + "summary": "Диагноз: ...\nПлан:\n1) ...\n2) ...\nПроверка: pytest -q", + "context_requests": [ + { "type": "read_file", "path": "src/app.py", "start_line": 1, "end_line": 240 } + ], + "memory_patch": { "user.output_format": "plan" } +} +``` + +### Как приложение обрабатывает ответ + +1. Парсит JSON из ответа (извлекает из ```json ... ``` при наличии). +2. Берёт `actions` из корня или `proposed_changes.actions`. +3. Валидирует: path (no `../`, no absolute), content обязателен для CREATE_FILE/UPDATE_FILE. +4. `summary` используется если есть; иначе формируется в коде. +5. `context_requests` — выполняется в следующем раунде (до MAX_CONTEXT_ROUNDS). +6. `memory_patch` — применяется только ключи из whitelist. + +--- + +## context_requests (типы запросов) + +| type | Обязательные поля | Описание | +|------------|-------------------|----------| +| `read_file`| path | Прочитать файл (опционально start_line, end_line) | +| `search` | query | Поиск по проекту (опционально glob) | +| `logs` | source | Логи (приложение ограничено) | +| `env` | — | Информация об окружении | + +--- + +## Автосбор контекста (до первого вызова LLM) + +Эвристики по содержимому user_goal и отчёта: + +- **Traceback / Exception** → извлекаются пути и номера строк, читаются файлы ±80 строк вокруг +- **ImportError / ModuleNotFoundError / cannot find module** → добавляются ENV + содержимое pyproject.toml, requirements.txt, package.json, poetry.lock + +--- + +## Типы в Rust (справочно) + +- `Action`: `{ kind: ActionKind, path: String, content: Option }` +- `ActionKind`: enum `CreateFile | CreateDir | UpdateFile | DeleteFile | DeleteDir` (сериализуется в SCREAMING_SNAKE_CASE) +- `AgentPlan`: `{ ok: bool, summary: String, actions: Vec, error?: String, error_code?: String }` + +--- + +## memory_patch + whitelist + пример промпта (под этот контракт) + +### 1) memory_patch (что подставлять в промпт как контекст «памяти») + +Хранить в приложении (файл/БД/локальное хранилище) и подставлять в system или в начало user-сообщения: + +```json +{ + "preferred_style": "коротко, по делу", + "default_language": "python", + "test_command": "pytest -q", + "lint_command": "ruff check .", + "format_command": "ruff format .", + "project_root_hint": "src/ — код, tests/ — тесты" +} +``` + +В промпте: один абзац, например: +«Память: стиль — коротко по делу; язык по умолчанию — python; тесты — pytest -q; линт — ruff check .; структура — src/, tests/.» + +### 2) whitelist (разрешённые пути для действий) + +При парсинге плана и перед apply проверять: все `path` должны быть относительно корня проекта и не выходить за его пределы. Дополнительно можно ограничить типы файлов/папок. + +Пример whitelist (Rust/конфиг): + +- Разрешены расширения для CREATE_FILE/UPDATE_FILE: `.py`, `.ts`, `.tsx`, `.js`, `.jsx`, `.json`, `.md`, `.yaml`, `.yml`, `.toml`, `.css`, `.html`, `.sql`, `.sh`, `.env.example`, без расширения — только известные имена: `README`, `Makefile`, `.gitignore`, `.editorconfig`, `Dockerfile`. +- Запрещены пути: содержащие `..`, абсолютные пути, `.env` (без .example), `*.key`, `*.pem`, каталоги `node_modules/`, `.git/`, `__pycache__/`. + +Файл конфига (например `config/llm_whitelist.json`): + +```json +{ + "allowed_extensions": [".py", ".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml", ".toml", ".css", ".html", ".sql", ".sh"], + "allowed_no_extension": ["README", "Makefile", ".gitignore", ".editorconfig", "Dockerfile", "LICENSE"], + "forbidden_paths": [".env", "*.key", "*.pem", "node_modules", ".git", "__pycache__"], + "forbidden_prefixes": ["..", "/"] +} +``` + +### 3) Пример промпта (фрагмент под твой JSON-контракт) + +Добавить в промпт явное напоминание формата ответа: + +```text +Верни ТОЛЬКО валидный JSON — массив действий, без markdown и пояснений. +Формат каждого элемента: { "kind": "CREATE_FILE" | "CREATE_DIR" | "UPDATE_FILE" | "DELETE_FILE" | "DELETE_DIR", "path": "относительный/путь", "content": "опционально для CREATE_FILE/UPDATE_FILE" }. +Пример: [{"kind":"CREATE_FILE","path":"README.md","content":"# Project\n"},{"kind":"CREATE_DIR","path":"src"}] +``` + +Это уже есть в build_prompt; при добавлении memory_patch в начало user-сообщения можно добавить блок: + +```text +Память (предпочтения оператора): preferred_style=коротко по делу; default_language=python; test_command=pytest -q; lint_command=ruff check .; format_command=ruff format .; структура проекта: src/, tests/. +``` + +После применения whitelist при apply отклонять действия с path вне whitelist и возвращать в AgentPlan error с кодом FORBIDDEN_PATH. + +--- + +## Инженерная память (Engineering Memory) + +Память разделена на три слоя; в промпт подставляется только устойчивый минимум (~1–2 KB). + +### A) User prefs (оператор) + +- Расположение: `app_data_dir()/papa-yu/preferences.json` (локально). +- Поля: `preferred_style` (brief|normal|verbose), `ask_budget` (0..2), `risk_tolerance` (low|medium|high), `default_language`, `output_format` (patch_first|plan_first). + +### B) Project prefs (для репо) + +- Расположение: в репо `.papa-yu/project.json` (шарится между машинами при коммите). +- Поля: `default_test_command`, `default_lint_command`, `default_format_command`, `package_manager`, `build_command`, `src_roots`, `test_roots`, `ci_notes`. + +### C) Session state + +- В памяти процесса (не в файлах): current_task_goal, current_branch, recent_files, recent_errors. В текущей реализации не подставляется в промпт. + +### MEMORY BLOCK в промпте + +Добавляется в **system message** после основного system prompt: + +```text +ENGINEERING_MEMORY (trusted by user; update only when user requests): +{"user":{"preferred_style":"brief","ask_budget":1,"risk_tolerance":"medium","default_language":"python"},"project":{"default_test_command":"pytest -q","default_lint_command":"ruff check .","default_format_command":"ruff format .","src_roots":["src"],"test_roots":["tests"]}} + +Use ENGINEERING_MEMORY as defaults. If user explicitly asks to change — suggest updating memory and show new JSON. +``` + +### Ответ с memory_patch + +Если пользователь просит «запомни, что тесты запускать так-то», LLM может вернуть объект: + +```json +{ + "actions": [], + "memory_patch": { + "project.default_test_command": "pytest -q", + "user.preferred_style": "brief" + } +} +``` + +Приложение применяет только ключи из whitelist и сохраняет в `preferences.json` / `.papa-yu/project.json`. + +**Безопасность memory_patch:** при парсинге удаляются все ключи не из whitelist; валидируются типы (`ask_budget` int, `src_roots` массив строк и т.д.). Рекомендуется применять patch только при явной просьбе пользователя. + +### Whitelist memory_patch (ключи через точку) + +- `user.preferred_style`, `user.ask_budget`, `user.risk_tolerance`, `user.default_language`, `user.output_format` +- `project.default_test_command`, `project.default_lint_command`, `project.default_format_command`, `project.package_manager`, `project.build_command`, `project.src_roots`, `project.test_roots`, `project.ci_notes` + +Примеры файлов: см. `docs/preferences.example.json` и `docs/project.example.json`. diff --git a/docs/OPENAI_SETUP.md b/docs/OPENAI_SETUP.md new file mode 100644 index 0000000..78f3f4f --- /dev/null +++ b/docs/OPENAI_SETUP.md @@ -0,0 +1,125 @@ +# Подключение PAPA-YU к OpenAI + +Инструкция по настройке кнопки **«Предложить исправления»** для работы через API OpenAI. + +--- + +## 1. Получение API-ключа OpenAI + +1. Зайдите на [platform.openai.com](https://platform.openai.com). +2. Войдите в аккаунт или зарегистрируйтесь. +3. Откройте **API keys** (раздел **Settings** → **API keys** или [прямая ссылка](https://platform.openai.com/api-keys)). +4. Нажмите **Create new secret key**, задайте имя (например, `PAPA-YU`) и скопируйте ключ. +5. Сохраните ключ в надёжном месте — повторно его показать нельзя. + +--- + +## 2. Переменные окружения + +Перед запуском приложения задайте три переменные. + +### Обязательные + +| Переменная | Значение | Описание | +|------------|----------|----------| +| `PAPAYU_LLM_API_URL` | `https://api.openai.com/v1/chat/completions` | URL эндпоинта Chat Completions OpenAI. | +| `PAPAYU_LLM_API_KEY` | Ваш API-ключ OpenAI | Ключ передаётся в заголовке `Authorization: Bearer <ключ>`. | + +### Опциональные + +| Переменная | Значение по умолчанию | Описание | +|------------|------------------------|----------| +| `PAPAYU_LLM_MODEL` | `gpt-4o-mini` | Модель для генерации плана (например, `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`). | +| `PAPAYU_LLM_MODE` | `chat` | Режим агента: `chat` (инженер-коллега) или `fixit` (обязан вернуть патч + проверку). См. `docs/AGENT_CONTRACT.md`. | + +--- + +## 3. Запуск с OpenAI + +### Вариант A: В текущей сессии терминала (macOS / Linux) + +```bash +cd /Users/yrippertgmail.com/Desktop/papa-yu + +export PAPAYU_LLM_API_URL="https://api.openai.com/v1/chat/completions" +export PAPAYU_LLM_API_KEY="sk-ваш-ключ-openai" +export PAPAYU_LLM_MODEL="gpt-4o-mini" + +npm run tauri dev +``` + +Подставьте вместо `sk-ваш-ключ-openai` свой ключ. + +### Вариант B: Одна строкой (без сохранения ключа в истории) + +```bash +cd /Users/yrippertgmail.com/Desktop/papa-yu +PAPAYU_LLM_API_URL="https://api.openai.com/v1/chat/completions" \ +PAPAYU_LLM_API_KEY="sk-ваш-ключ" \ +PAPAYU_LLM_MODEL="gpt-4o-mini" \ +npm run tauri dev +``` + +### Вариант C: Файл `.env` в корне проекта (если приложение его подхватывает) + +В PAPA-YU переменные читаются из окружения процесса. Tauri сам по себе не загружает `.env`. Чтобы использовать `.env`, можно запускать через `env` или скрипт: + +```bash +# В papa-yu создайте файл .env (добавьте .env в .gitignore, чтобы не коммитить ключ): +# PAPAYU_LLM_API_URL=https://api.openai.com/v1/chat/completions +# PAPAYU_LLM_API_KEY=sk-ваш-ключ +# PAPAYU_LLM_MODEL=gpt-4o-mini + +# Запуск с подгрузкой .env (macOS/Linux, если установлен dotenv-cli): +# npm install -g dotenv-cli +# dotenv -e .env -- npm run tauri dev +``` + +Или простой скрипт `start-with-openai.sh`: + +```bash +#!/bin/bash +cd "$(dirname "$0")" +set -a +source .env # или export переменные здесь +set +a +npm run tauri dev +``` + +--- + +## 4. Проверка + +1. Запустите приложение с заданными переменными. +2. Выберите проект (папку или путь). +3. Запустите **Анализ**. +4. Введите цель (например: «Добавить README и .gitignore»). +5. Нажмите **«Предложить исправления»**. + +Если всё настроено верно, план будет сформирован через OpenAI. В случае ошибки в интерфейсе или в логах будет указание на API (например, 401 — неверный ключ, 429 — лимиты). + +--- + +## 5. Безопасность + +- Не коммитьте API-ключ в репозиторий и не вставляйте его в скрипты, которые попадают в историю. +- Добавьте `.env` в `.gitignore`, если храните ключ в `.env`. +- При утечке ключа отзовите его в [OpenAI API keys](https://platform.openai.com/api-keys) и создайте новый. + +--- + +## 6. Другие модели OpenAI + +Можно указать другую модель через `PAPAYU_LLM_MODEL`, например: + +- `gpt-4o` — более способная модель. +- `gpt-4o-mini` — быстрее и дешевле (по умолчанию в коде). +- `gpt-4-turbo` — баланс качества и скорости. + +Актуальный список и цены: [OpenAI Pricing](https://openai.com/pricing). + +--- + +## 7. Если переменные не заданы + +Если `PAPAYU_LLM_API_URL` не задана или пустая, кнопка **«Предложить исправления»** работает без API: используется встроенная эвристика (правила для README, .gitignore, LICENSE, .env.example и т.п.). diff --git a/docs/TEST-AUTO-ROLLBACK.md b/docs/TEST-AUTO-ROLLBACK.md new file mode 100644 index 0000000..3e5e5fe --- /dev/null +++ b/docs/TEST-AUTO-ROLLBACK.md @@ -0,0 +1,30 @@ +# Тест AUTO_ROLLBACK (v2.3.3) + +Проверка: первый шаг применяется, второй падает → откат первого шага, в UI сообщения «Обнаружены ошибки. Откатываю изменения…» и «Изменения привели к ошибкам, откат выполнен.» + +## Формат payload в papa-yu + +- Команда: `apply_actions` (или `apply_actions_cmd`). +- Payload: `ApplyPayload` с полями **`root_path`**, **`actions`**, **`auto_check`**. +- В **`actions`** поле **`kind`** в формате **SCREAMING_SNAKE_CASE**: `CREATE_FILE`, `UPDATE_FILE`, `DELETE_FILE`, `CREATE_DIR`, `DELETE_DIR`. + +## Вариант 1 — падение на safe_join (..) + +Подставь свой путь в `root_path` и вызови apply с `actions` из `test-auto-rollback-payload.json`: + +1. Создаётся `papayu_test_ok.txt`. +2. Второй action с путём `../../forbidden.txt` → `safe_join` возвращает ошибку. +3. Rollback удаляет `papayu_test_ok.txt`. +4. Ответ: `ok: false`, `error_code: "AUTO_ROLLBACK_DONE"`, `failed_at: 1`. + +## Вариант 2 — падение через ОС (permission denied) + +Используй `test-auto-rollback-fs-payload.json`: второй шаг пишет в `/System/...` — в papa-yu абсолютный путь отсекается в `safe_join`, так что отказ будет до записи в ФС, результат тот же (AUTO_ROLLBACK_DONE + откат). + +## Запуск + +```bash +cd ~/Desktop/papa-yu/src-tauri && cargo tauri dev +``` + +Проверку можно делать через UI (предпросмотр → применить с действиями, которые содержат запрещённый путь) или через invoke с payload из JSON выше. diff --git a/docs/fix_plan_response_schema.json b/docs/fix_plan_response_schema.json new file mode 100644 index 0000000..94eb9b1 --- /dev/null +++ b/docs/fix_plan_response_schema.json @@ -0,0 +1,75 @@ +{ + "name": "papa_yu_plan_response", + "strict": false, + "schema": { + "type": "object", + "properties": { + "mode": { "type": "string", "enum": ["fix-plan", "apply"] }, + "summary": { "type": "string" }, + "questions": { "type": "array", "items": { "type": "string" } }, + "context_requests": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["read_file", "search", "logs", "env"] }, + "path": { "type": "string" }, + "start_line": { "type": "integer" }, + "end_line": { "type": "integer" }, + "query": { "type": "string" }, + "glob": { "type": "string" }, + "source": { "type": "string" }, + "last_n": { "type": "integer" } + } + } + }, + "plan": { + "type": "array", + "items": { + "type": "object", + "properties": { + "step": { "type": "string" }, + "details": { "type": "string" } + } + } + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { "type": "string", "enum": ["CREATE_FILE", "CREATE_DIR", "UPDATE_FILE", "DELETE_FILE", "DELETE_DIR"] }, + "path": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["kind", "path"] + } + }, + "proposed_changes": { + "type": "object", + "properties": { + "patch": { "type": "string" }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { "type": "string" }, + "path": { "type": "string" }, + "content": { "type": "string" } + } + } + }, + "commands_to_run": { "type": "array", "items": { "type": "string" } } + } + }, + "patch": { "type": "string" }, + "commands_to_run": { "type": "array", "items": { "type": "string" } }, + "verification": { "type": "array", "items": { "type": "string" } }, + "risks": { "type": "array", "items": { "type": "string" } }, + "rollback": { "type": "array", "items": { "type": "string" } }, + "memory_patch": { "type": "object" } + }, + "additionalProperties": true + } +} diff --git a/docs/openai_tools_schema.json b/docs/openai_tools_schema.json new file mode 100644 index 0000000..2aa26f4 --- /dev/null +++ b/docs/openai_tools_schema.json @@ -0,0 +1,121 @@ +[ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List project files (optionally under a directory).", + "parameters": { + "type": "object", + "properties": { + "root": { "type": "string", "description": "Directory root, e.g. '.' or 'src'." }, + "glob": { "type": "string", "description": "Glob filter, e.g. '**/*.py'." }, + "max_results": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 } + }, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read a text file (optionally a line range).", + "parameters": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "start_line": { "type": "integer", "minimum": 1, "description": "1-based inclusive." }, + "end_line": { "type": "integer", "minimum": 1, "description": "1-based inclusive." }, + "max_chars": { "type": "integer", "minimum": 200, "maximum": 200000, "default": 40000 } + }, + "required": ["path"] + } + } + }, + { + "type": "function", + "function": { + "name": "search_in_repo", + "description": "Search for a pattern across the repository.", + "parameters": { + "type": "object", + "properties": { + "query": { "type": "string", "description": "Literal or regex depending on implementation." }, + "glob": { "type": "string", "description": "Optional glob scope." }, + "case_sensitive": { "type": "boolean", "default": false }, + "max_results": { "type": "integer", "minimum": 1, "maximum": 500, "default": 50 } + }, + "required": ["query"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_env", + "description": "Get environment info: OS, language versions, package manager, tool versions.", + "parameters": { "type": "object", "properties": {}, "required": [] } + } + }, + { + "type": "function", + "function": { + "name": "run", + "description": "Run a command (lint/build/custom).", + "parameters": { + "type": "object", + "properties": { + "command": { "type": "string" }, + "cwd": { "type": "string", "default": "." }, + "timeout_sec": { "type": "integer", "minimum": 1, "maximum": 600, "default": 120 } + }, + "required": ["command"] + } + } + }, + { + "type": "function", + "function": { + "name": "run_tests", + "description": "Run the project's test suite with a command.", + "parameters": { + "type": "object", + "properties": { + "command": { "type": "string", "description": "e.g. 'pytest -q' or 'npm test'." }, + "cwd": { "type": "string", "default": "." }, + "timeout_sec": { "type": "integer", "minimum": 1, "maximum": 1800, "default": 600 } + }, + "required": ["command"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_logs", + "description": "Get recent application logs / build logs / runtime logs.", + "parameters": { + "type": "object", + "properties": { + "source": { "type": "string", "description": "e.g. 'app', 'build', 'runtime'." }, + "last_n": { "type": "integer", "minimum": 1, "maximum": 5000, "default": 200 } + }, + "required": ["source"] + } + } + }, + { + "type": "function", + "function": { + "name": "apply_patch", + "description": "Apply a unified diff patch to the repository.", + "parameters": { + "type": "object", + "properties": { + "diff": { "type": "string", "description": "Unified diff" } + }, + "required": ["diff"] + } + } + } +] diff --git a/docs/papa_yu_response_schema.json b/docs/papa_yu_response_schema.json new file mode 100644 index 0000000..fe965d9 --- /dev/null +++ b/docs/papa_yu_response_schema.json @@ -0,0 +1,92 @@ +{ + "name": "papa_yu_response", + "description": "JSON Schema для response_format LLM (OpenAI Responses API, строгий JSON-вывод). Принимает: массив actions ИЛИ объект с полями.", + "schema": { + "oneOf": [ + { + "type": "array", + "description": "Прямой массив действий (обратная совместимость)", + "items": { "$ref": "#/$defs/action" }, + "minItems": 0 + }, + { + "type": "object", + "description": "Объект Fix-plan: actions, summary, context_requests, memory_patch", + "additionalProperties": true, + "properties": { + "mode": { + "type": "string", + "enum": ["fix-plan", "apply"], + "description": "Опционально: fix-plan = план без изменений, apply = план с действиями" + }, + "actions": { + "type": "array", + "items": { "$ref": "#/$defs/action" } + }, + "proposed_changes": { + "type": "object", + "additionalProperties": true, + "properties": { + "actions": { + "type": "array", + "items": { "$ref": "#/$defs/action" } + } + } + }, + "summary": { "type": "string" }, + "questions": { "type": "array", "items": { "type": "string" } }, + "context_requests": { + "type": "array", + "items": { "$ref": "#/$defs/context_request" } + }, + "plan": { + "type": "array", + "items": { + "type": "object", + "properties": { "step": { "type": "string" }, "details": { "type": "string" } } + } + }, + "memory_patch": { + "type": "object", + "additionalProperties": true, + "description": "Только ключи из whitelist: user.*, project.*" + }, + "risks": { "type": "array", "items": { "type": "string" } } + } + } + ], + "$defs": { + "action": { + "type": "object", + "additionalProperties": true, + "required": ["kind", "path"], + "properties": { + "kind": { + "type": "string", + "enum": ["CREATE_FILE", "CREATE_DIR", "UPDATE_FILE", "DELETE_FILE", "DELETE_DIR"] + }, + "path": { "type": "string" }, + "content": { + "type": "string", + "description": "Обязательно для CREATE_FILE и UPDATE_FILE" + } + } + }, + "context_request": { + "type": "object", + "additionalProperties": true, + "required": ["type"], + "properties": { + "type": { "type": "string", "enum": ["read_file", "search", "logs", "env"] }, + "path": { "type": "string" }, + "start_line": { "type": "integer", "minimum": 1 }, + "end_line": { "type": "integer", "minimum": 1 }, + "query": { "type": "string" }, + "glob": { "type": "string" }, + "source": { "type": "string" }, + "last_n": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + } + } + } +} diff --git a/docs/preferences.example.json b/docs/preferences.example.json new file mode 100644 index 0000000..6b6bb69 --- /dev/null +++ b/docs/preferences.example.json @@ -0,0 +1,10 @@ +{ + "schema_version": 1, + "user": { + "preferred_style": "brief", + "ask_budget": 1, + "risk_tolerance": "medium", + "default_language": "python", + "output_format": "plan_first" + } +} diff --git a/docs/project.example.json b/docs/project.example.json new file mode 100644 index 0000000..fedbfec --- /dev/null +++ b/docs/project.example.json @@ -0,0 +1,13 @@ +{ + "schema_version": 1, + "project": { + "default_test_command": "pytest -q", + "default_lint_command": "ruff check .", + "default_format_command": "ruff format .", + "package_manager": "pip", + "build_command": "python -c \"import app.main; print('ok')\"", + "src_roots": ["src"], + "test_roots": ["tests"], + "ci_notes": "Тесты долго, по умолчанию smoke." + } +} diff --git a/docs/test-auto-rollback-fs-payload.json b/docs/test-auto-rollback-fs-payload.json new file mode 100644 index 0000000..af8d0cc --- /dev/null +++ b/docs/test-auto-rollback-fs-payload.json @@ -0,0 +1,17 @@ +{ + "_comment": "v2.3.3 — тест AUTO_ROLLBACK через ОС: шаг 2 пишет в /System/... → permission denied. kind = SCREAMING_SNAKE_CASE.", + "root_path": "/ПУТЬ/К/ПРОЕКТУ", + "actions": [ + { + "kind": "CREATE_FILE", + "path": "rollback_test.txt", + "content": "will be rolled back" + }, + { + "kind": "UPDATE_FILE", + "path": "/System/Library/forbidden.txt", + "content": "permission denied" + } + ], + "auto_check": false +} diff --git a/docs/test-auto-rollback-payload.json b/docs/test-auto-rollback-payload.json new file mode 100644 index 0000000..323c729 --- /dev/null +++ b/docs/test-auto-rollback-payload.json @@ -0,0 +1,17 @@ +{ + "_comment": "v2.3.3 — тест AUTO_ROLLBACK: шаг 1 создаёт файл, шаг 2 падает на safe_join (..). В papa-yu kind = SCREAMING_SNAKE_CASE.", + "root_path": "/ПУТЬ/К/ПРОЕКТУ", + "actions": [ + { + "kind": "CREATE_FILE", + "path": "papayu_test_ok.txt", + "content": "Эта строка будет создана, а потом удалена rollback-ом" + }, + { + "kind": "UPDATE_FILE", + "path": "../../forbidden.txt", + "content": "Эта операция должна упасть из-за safe_join" + } + ], + "auto_check": false +} diff --git a/docs/ЕДИНАЯ_ПАПКА_ПРОЕКТА.md b/docs/ЕДИНАЯ_ПАПКА_ПРОЕКТА.md new file mode 100644 index 0000000..dc1c096 --- /dev/null +++ b/docs/ЕДИНАЯ_ПАПКА_ПРОЕКТА.md @@ -0,0 +1,71 @@ +# Единая папка проекта PAPA YU + +После объединения **вся разработка, сборка и запуск** десктопного приложения ведутся из одной папки. Папка **папа-ю** (документы) не переносилась — по вашему требованию она остаётся отдельно. + +--- + +## Проверенные пути (состояние на момент объединения) + +| Путь | Существование | Содержимое | +|------|----------------|------------| +| `/Users/yrippertgmail.com/Desktop/papa-yu` | ✅ | **Единая папка проекта.** Код (src/, src-tauri/), скрипты, docs. Здесь вносятся правки и собирается приложение. | +| `/Users/yrippertgmail.com/Desktop/папа-ю` | ✅ | Только документы и ТЗ. **Не переносилась.** Ссылка в README. | +| `/Users/yrippertgmail.com/PAPA-YU` | ✅ | Та же файловая система, что и `~/papa-yu` (один inode на macOS). Другая структура: desktop/ui, desktop/src-tauri. | +| `/Users/yrippertgmail.com/papa-yu` | ✅ | То же, что PAPA-YU (одна папка в домашнем каталоге). | + +**Итог:** физически есть две разные копии кода: +1. **Desktop/papa-yu** — плоская структура (src/, src-tauri/ в корне), актуальные правки, диалог с агентом, рекомендации ИИ и т.д. +2. **~/papa-yu** (= ~/PAPA-YU) — вложенная структура (desktop/ui, desktop/src-tauri), другая версия (другие страницы, плагин updater). + +Объединение выполнено так: **единой рабочей папкой выбран Desktop/papa-yu.** Из домашней копии перенесены только идеи скриптов запуска; пути и скрипты переписаны под структуру Desktop/papa-yu. + +--- + +## Что сделано в Desktop/papa-yu + +1. **Добавлены кнопки запуска (двойной клик):** + - **`PAPA YU.command`** — только запуск собранного приложения (.app). Если сборки ещё нет, скрипт подскажет запустить «Сборка и запуск». + - **`PAPA YU — Сборка и запуск.command`** — первая сборка или после обновления кода: `npm run tauri build`, затем открытие .app. + +2. **Пути в скриптах:** оба .command используют `$(dirname "$0")` — работают из любой текущей директории, привязаны к папке, где лежит скрипт (корень Desktop/papa-yu). Сборка ищет .app в `src-tauri/target/release/bundle/macos/`. + +3. **Документация:** в `docs/` обновлён файл рекомендаций; этот файл (`ЕДИНАЯ_ПАПКА_ПРОЕКТА.md`) фиксирует канонический путь и связи. + +4. **README:** добавлена секция «Единственная папка проекта» с путём и способами запуска. + +--- + +## Что сделать вам (рекомендации) + +1. **Работать только из одной папки:** + `/Users/yrippertgmail.com/Desktop/papa-yu` + Все изменения кода, сборка, запуск — только здесь. + +2. **Домашняя копия (~/papa-yu / ~/PAPA-YU):** + Чтобы не путать две копии, после проверки работы из Desktop можно переименовать домашнюю, например: + `mv ~/papa-yu ~/papa-yu-archive` + Или удалить, если архив не нужен. Перед удалением убедитесь, что в Desktop/papa-yu есть всё нужное (в т.ч. .env.openai при необходимости скопировать вручную). + +3. **Папка папа-ю:** + Не трогать. ТЗ и спецификации остаются в `Desktop/папа-ю`. В README указано, где они лежат. + +4. **Пересборка после объединения:** + - Открыть терминал. + - `cd /Users/yrippertgmail.com/Desktop/papa-yu` + - `npm install` (если ещё не выполняли). + - Запуск для разработки: `./start-with-openai.sh` или `npm run tauri dev`. + - Сборка .app: двойной клик по **«PAPA YU — Сборка и запуск.command»** или `npm run tauri build`. + После сборки запуск — двойной клик по **«PAPA YU.command»**. + +--- + +## Связи по новой + +| Кто | Ссылается на | +|-----|----------------| +| **Код, сборка, запуск** | Только `Desktop/papa-yu` | +| **ТЗ и спецификации** | Папка `Desktop/папа-ю` (в README/docs указан путь) | +| **PAPA YU.command** | Ищет .app в `Desktop/papa-yu/src-tauri/target/release/bundle/macos/` | +| **Сборка и запуск.command** | Выполняет `npm run tauri build` в `Desktop/papa-yu` | + +Жёстко прописанных путей в исходном коде (src, src-tauri) нет — везде относительные пути от корня проекта. Ошибки путей после объединения не ожидаются, если открывать и собирать проект из `Desktop/papa-yu`. diff --git a/docs/ОЦЕНКА_И_СЦЕНАРИЙ_РАССКАЗА.md b/docs/ОЦЕНКА_И_СЦЕНАРИЙ_РАССКАЗА.md new file mode 100644 index 0000000..9fcde33 --- /dev/null +++ b/docs/ОЦЕНКА_И_СЦЕНАРИЙ_РАССКАЗА.md @@ -0,0 +1,141 @@ +# PAPA YU: оценка обновлений и сценарий рассказа о программе + +--- + +## Часть 1. Нуждается ли программа в обновлении или улучшении? + +### Текущее состояние (после внедрённых улучшений) + +**Уже сделано:** +- **Архитектура:** типы вынесены в `src/lib/types.ts`, единый API-слой в `src/lib/tauri.ts`; страница Tasks разбита на компоненты PathSelector, AgenticResult и хук useUndoRedo. +- **Безопасность (v2.4.4):** лимиты профиля (`max_actions_per_tx`, `timeout_sec`) применяются в `apply_actions_tx` и `run_batch`; в `verify_project` добавлен таймаут на выполнение проверок (cargo check, npm run build и т.д.). +- **Иконка:** используется RGBA-иконка из папки «папа-ю», сборка Tauri проходит успешно. + +**Программа работоспособна и готова к использованию.** При этом остаются направления для развития. + +### Что имеет смысл улучшить (по приоритету) + +| Область | Рекомендация | Зачем | +|--------|---------------|--------| +| **UX** | История сессий в UI, отображение профиля и лимитов в форме, фильтр расширений при «Прикрепить файл», горячие клавиши (Ctrl+Enter, Escape) | Удобство и прозрачность для пользователя | +| **Тестирование** | Юнит-тесты для `detect_project_type`, `is_protected_file`, `build_plan`; E2E-сценарии (анализ → применение → undo) | Надёжность и уверенность при изменениях | +| **Документация** | Обновить README до актуальной версии (2.4.x), вести CHANGELOG | Проще онбординг и откат версий | +| **Дальнейшие фичи** | LLM-планировщик вместо чисто эвристического плана; контекст прикреплённых файлов при анализе; allowlist команд verify в конфиге | Расширение возможностей и гибкость | + +**Вывод:** программа не «сломана» и не требует срочного обновления для базового сценария. Улучшения из списка выше повысят удобство, предсказуемость и расширяемость — их можно планировать поэтапно. + +--- + +## Часть 2. Сценарий рассказа о программе (по модулям, технологиям, защите и уникальности) + +Ниже — готовый сценарий для устного или письменного рассказа о PAPA YU: модули, достоинства, технологии, защищённость, возможности и уникальность. + +--- + +### Введение + +PAPA YU — это десктопное приложение для анализа кодовой базы и безопасного автоматического улучшения проектов: добавление README, .gitignore, тестов, приведение структуры к принятым практикам. Всё это делается с полным контролем пользователя: предпросмотр изменений, подтверждение применения и возможность отката одной кнопкой. + +Рассказ удобно строить по слоям: интерфейс, единый слой работы с бэкендом, бэкенд по модулям, безопасность и уникальность. + +--- + +### Модуль 1. Интерфейс (React + Vite) + +**Что это:** одностраничное приложение на React 18 и Vite 5. Одна основная страница — «Задачи» (Tasks): выбор папок и файлов, поле ввода пути, лента сообщений (чат с системой и ассистентом), блоки с отчётом, превью изменений и результатами автоматического прогона. + +**Модульная структура фронта:** +- **PathSelector** — блок выбора папок и прикреплённых файлов, кнопки «Выбрать папку» и «Прикрепить файл», список выбранных путей. +- **AgenticResult** — блок результата «исправить автоматически»: таблица попыток, статусы проверок, кнопки «Скачать отчёт», «Скачать diff», «Откатить». +- **useUndoRedo** — хук для состояния отката/повтора и вызова команд undo/redo через единый API-слой. +- **Единая точка типов и API:** все типы (Action, AnalyzeReport, AgenticRunResult и др.) лежат в `src/lib/types.ts`; все вызовы бэкенда идут через `src/lib/tauri.ts`, без прямых `invoke` в компонентах. + +**Достоинства:** понятная структура, переиспользуемые компоненты и типы, один слой общения с Tauri — проще поддерживать и тестировать. + +**Технологии:** React 18, Vite 5, TypeScript, React Router, Tauri 2 (плагины: dialog, shell). Алиас `@/` для импортов, строгий TypeScript. + +--- + +### Модуль 2. Единый слой API (src/lib/tauri.ts) + +**Что это:** все обращения к бэкенду проходят через функции в `tauri.ts`: `getProjectProfile`, `runBatchCmd`, `applyActionsTx`, `agenticRun`, `generateActionsFromReport`, `proposeActions`, `undoLastTx`, `undoLast`, `redoLast`, работа с папками и сессиями и т.д. + +**Достоинства:** один контракт между UI и Rust; проще менять форматы запросов/ответов, добавлять логирование и обработку ошибок без правок во всех компонентах. + +**Технологии:** `@tauri-apps/api/core` (invoke), типы из `@/lib/types`. + +--- + +### Модуль 3. Бэкенд: команды и оркестрация (Tauri 2, Rust) + +**Структура по модулям:** + +- **analyze_project** — сканирование выбранных путей, эвристики (README, .gitignore, tests, структура), формирование отчёта с findings, recommendations, actions, action_groups, fix_packs. +- **get_project_profile** — определение типа проекта (React/Vite, Next.js, Node, Rust, Python, unknown) по файлам в корне; возврат профиля с лимитами (`max_actions_per_tx`, `timeout_sec`, `max_files`), шаблоном цели и флагом safe_mode. +- **run_batch** — единый сценарий: анализ → превью (если есть действия) → при необходимости применение с проверками. Перед применением проверяется лимит `max_actions_per_tx` из профиля; при превышении возвращается ошибка. +- **apply_actions_tx** — транзакционное применение: снимок состояния до изменений, применение действий, при включённом auto_check — запуск проверок (cargo check / npm run build и т.д.) с таймаутом из профиля; при падении проверки — автоматический откат. Проверка числа действий против `max_actions_per_tx`. +- **preview_actions** — расчёт diff без записи на диск. +- **undo_last_tx / undo_last / redo_last, get_undo_redo_state_cmd, undo_status** — откат и повтор последней транзакции, состояние undo/redo для UI. +- **generate_actions_from_report** — генерация списка безопасных действий по отчёту (например, только создание файлов/папок) без LLM. +- **propose_actions** — план исправлений по отчёту и цели пользователя (эвристический планировщик). +- **agentic_run** — цикл: анализ → план → превью → применение → проверка (verify); при неудаче проверки — откат и повтор в пределах max_attempts. +- **verify** — проверка проекта после изменений (cargo check, npm run build/test и т.д.) по allowlist команд, с таймаутом на каждый запуск. +- **projects / store** — проекты и сессии (list_projects, add_project, append_session_event, list_sessions), хранение в userData. + +**Достоинства:** чёткое разделение по командам, переиспользование типов и лимитов профиля, единый контур «превью → применение → проверка → откат при ошибке». + +**Технологии:** Rust, Tauri 2, serde, uuid, стандартная библиотека (fs, path, process, thread, time), allowlist команд и таймауты для внешних процессов. + +--- + +### Модуль 4. Транзакции и откат (tx/) + +**Что это:** снимки состояния файлов до применения, запись манифестов транзакций, откат к снимку при падении проверок или по запросу пользователя, стек undo/redo. + +**Достоинства:** пользователь и система всегда могут откатить последнее применение; данные на диске остаются консистентными после отката. + +**Технологии:** копирование/восстановление файлов и директорий, манифесты в JSON, привязка к AppHandle и путям проекта. + +--- + +### Защищённость и ограничения рисков + +- **Защита путей:** запрет изменения системных и служебных путей (например, .git, node_modules, target, dist); проверка, что изменяются только допустимые текстовые файлы (`is_protected_file`, `is_text_allowed`). +- **Подтверждение пользователя:** применение изменений только при явном `user_confirmed`; в UI — кнопки «Применить» и при необходимости диалог подтверждения. +- **Лимиты из профиля:** ограничение числа действий в одной транзакции (`max_actions_per_tx`) и таймауты на проверки (`timeout_sec`) снижают риск «зависания» и перегрузки. +- **Allowlist команд в verify:** запускаются только заранее разрешённые команды (cargo check, npm run build и т.д.) с фиксированными аргументами, без произвольного shell. +- **Таймауты:** проверки (verify, auto_check) выполняются с ограничением по времени; при превышении процесс завершается, пользователь получает сообщение (например, TIMEOUT), откат при необходимости уже выполнен. + +В совокупности это даёт контролируемую среду: автоматизация без неограниченного доступа к системе и без «тихого» изменения критичных путей. + +--- + +### Возможности для пользователя + +- Выбор одной или нескольких папок и прикрепление файлов; ввод пути вручную. +- Один запуск анализа с получением отчёта: проблемы, рекомендации, группы действий и пакеты исправлений. +- Предпросмотр изменений (diff) до применения. +- Применение выбранных или рекомендованных исправлений с автоматической проверкой (сборка/тесты); при падении проверки — автоматический откат. +- Режим «исправить автоматически» (agentic run): несколько попыток с откатом при неудачной проверке. +- Режим «безопасные исправления в один клик»: генерация безопасных действий по отчёту → превью → применение с проверкой. +- Откат последнего применения и повтор (undo/redo). +- Скачивание отчёта и diff для аудита и отладки. +- Определение типа проекта и отображение профиля (в т.ч. лимитов) для прозрачности. + +--- + +### Уникальность + +- **Транзакционность и откат на уровне файловой системы:** не «патч и надежда», а снимок → применение → проверка → при ошибке откат к снимку и явное сообщение пользователю. +- **Профиль проекта и лимиты:** тип и лимиты (число действий, таймауты) задаются по структуре проекта и соблюдаются в apply и run_batch, что снижает риски и предсказуемо ограничивает автоматизацию. +- **Единый сценарий от анализа до отката:** анализ → план → превью → применение → проверка с таймаутом → откат при неудаче — реализован и в batch, и в agentic run, с одной и той же моделью безопасности. +- **Десктоп на Tauri 2:** нативный бэкенд на Rust, быстрый и контролируемый доступ к файлам и процессам, без веб-сервера и без открытия кода в браузере. +- **Гибрид эвристик и подготовки к LLM:** уже есть структура (propose_actions, agentic_run, отчёт с narrative и actions); планировщик можно заменить на LLM без смены контура выполнения. + +--- + +### Заключение сценария + +PAPA YU — это не просто «генератор README», а инструмент с чёткой архитектурой, контролем рисков и расширяемостью. Модули фронта и бэкенда разделены, типы и API централизованы, применение изменений транзакционно с откатом и проверками по таймауту и лимитам. Защита путей, подтверждение пользователя и allowlist команд делают автоматизацию предсказуемой и безопасной. Уникальность — в сочетании транзакционности, профилей проекта и единого сценария «анализ → превью → применение → проверка → откат при ошибке» в одном десктопном приложении. + +При необходимости следующий шаг — улучшения UX (история сессий, горячие клавиши, фильтр файлов), тесты и обновление README/CHANGELOG, а затем — опционально LLM-планировщик и контекст прикреплённых файлов. diff --git a/docs/РЕКОМЕНДАЦИИ_ОБЪЕДИНЕНИЕ_ПАПОК.md b/docs/РЕКОМЕНДАЦИИ_ОБЪЕДИНЕНИЕ_ПАПОК.md new file mode 100644 index 0000000..da0b018 --- /dev/null +++ b/docs/РЕКОМЕНДАЦИИ_ОБЪЕДИНЕНИЕ_ПАПОК.md @@ -0,0 +1,142 @@ +# Анализ папок проекта PAPA YU и рекомендации по объединению + +Документ описывает текущее состояние папок, связанных с PAPA YU, и итог объединения в одну рабочую папку (папка **папа-ю** по вашему требованию не переносилась). + +**Проверенные пути:** +- `/Users/yrippertgmail.com/Desktop/papa-yu` — **единая папка проекта** (код, сборка, скрипты). +- `/Users/yrippertgmail.com/Desktop/папа-ю` — только документы и ТЗ (не переносилась). +- `/Users/yrippertgmail.com/PAPA-YU` и `/Users/yrippertgmail.com/papa-yu` — одна и та же папка в домашнем каталоге (другая структура: desktop/ui, desktop/src-tauri); после объединения можно архивировать или удалить. + +Итог объединения: см. `docs/ЕДИНАЯ_ПАПКА_ПРОЕКТА.md`. + +--- + +## 1. Текущее состояние папок + +### 1.1. `papa-yu` (латиница, на рабочем столе) + +| Назначение | Содержимое | +|------------|------------| +| **Роль** | Единственная папка с исходным кодом десктопного приложения PAPA YU | +| **Стек** | React, Vite, TypeScript, Tauri 2, Rust | +| **Структура** | `src/`, `src-tauri/`, `package.json`, `docs/`, скрипты запуска (`start-with-openai.sh`), конфиги | +| **Запуск** | `npm run tauri dev` или `./start-with-openai.sh` | +| **Сборка** | `npm run tauri build` → .app в `src-tauri/target/release/bundle/macos/` | + +**Вывод:** это основная рабочая папка для кода и сборки. Все правки приложения должны вноситься здесь. + +--- + +### 1.2. `папа-ю` (кириллица, на рабочем столе) + +| Назначение | Содержимое | +|------------|------------| +| **Роль** | Только документация и ТЗ по проекту PAPA-YU | +| **Содержимое** | `ЗАПУСК_ПРИЛОЖЕНИЯ.txt`, `старт/` (этапы 1–7, DOCX), `коррект/`, `ТЗ/`, архивы zip | +| **Код** | Нет исходного кода приложения | +| **Ссылки** | В тексте указано: запуск из `papa-yu/` (например `papa-yu/Собрать и запустить PAPA-YU.command`) | + +**Вывод:** папка используется как хранилище спецификаций и инструкций. Для «одной папки» её можно либо оставить как внешний архив документов, либо привязать к `papa-yu` через ссылку/подпапку (см. ниже). + +--- + +### 1.3. «PAPA YU» как имя папки + +Отдельной папки с названием **PAPA YU** (с пробелом) на рабочем столе нет. +**PAPA YU** — это название приложения (окно, `tauri.conf.json`). На macOS при открытии пути типа `~/Desktop/PAPA-YU` с учётом регистра может открываться та же файловая система, что и для `papa-yu` (зависит от настроек тома). Имеет смысл считать «одной папкой проекта» именно **`papa-yu`** и все пути вести к ней. + +--- + +### 1.4. `papa-app` (на рабочем столе) + +| Назначение | Содержимое | +|------------|------------| +| **Роль** | Отдельное веб-приложение (Next.js), не десктоп PAPA YU | +| **Содержимое** | `app/`, `components/`, `lib/`, Next.js-конфиги, а также «Новая папка» / «Новая папка с объектами» с материалами по PAPA (SQL, DOCX, скриншоты) | +| **Связь с PAPA YU** | Общее имя «PAPA», но другой продукт (веб vs десктоп) | + +**Вывод:** для объединения именно **десктопного PAPA YU** `papa-app` не объединять с `papa-yu` в один репозиторий/проект. Документы по PAPA из `papa-app` при необходимости можно копировать в общую структуру документов (см. ниже). + +--- + +## 2. Рекомендуемая «одна папка» для загрузки, изменений и правок + +Цель: **всё, что касается десктопного приложения PAPA YU, вести из одной папки** — загрузка (clone/build), правки кода, запуск, сборка, документация. + +### 2.1. Базовая рекомендация: единая точка входа — `papa-yu` + +- **Загрузка / клонирование:** один репозиторий или один архив — папка **`papa-yu`**. +- **Изменения кода:** только в **`papa-yu`** (src, src-tauri, конфиги). +- **Запуск и сборка:** всегда из корня **`papa-yu`**: + - разработка: `cd papa-yu && npm run tauri dev` или `./start-with-openai.sh`; + - сборка: `cd papa-yu && npm run tauri build`. +- **Документация по приложению:** хранить внутри **`papa-yu/docs/`** (как сейчас: OPENAI_SETUP, E2E, IMPROVEMENTS и т.д.). Все инструкции по запуску/сборке ссылаются на пути относительно `papa-yu`. + +Итог: **«одна папка» = `papa-yu`**. Все операции с десктопным PAPA YU выполняются из неё. + +--- + +### 2.2. Как учесть папку `папа-ю` (документы), не перемещая файлы + +Варианты **без** физического переноса файлов (только рекомендации): + +1. **Оставить как есть** + - Рабочая папка — `papa-yu`. + - `папа-ю` — отдельный каталог с ТЗ и этапами. + - В `papa-yu/README.md` или в `docs/` один раз явно написать: «ТЗ и спецификации проекта лежат в папке `папа-ю` на рабочем столе (или по пути …)». + +2. **Ссылка в документации** + - В `papa-yu/docs/` добавить файл (например `СВЯЗЬ_С_ДОКУМЕНТАМИ.md`) с единственной строкой: где физически лежит `папа-ю` и что там (ТЗ, этапы, архивы). Все продолжают открывать код только в `papa-yu`, а документы — по этой ссылке. + +3. **Симлинк (если нужен «один корень»)** + - Внутри `papa-yu` создать, например, `docs/specs-from-papa-yu-cyrillic` → симлинк на `~/Desktop/папа-ю`. Тогда «всё видно» из одного дерева `papa-yu`, но файлы кириллической папки не копируются. Рекомендация: делать только если действительно нужен единый корень в проводнике/IDE. + +--- + +### 2.3. Как учесть `papa-app` + +- **Не объединять** с `papa-yu` в один проект/репозиторий: разный стек и назначение. +- Если нужно хранить общие материалы по бренду/продукту PAPA: + - либо оставить их в `papa-app` и в `papa-yu/README.md` кратко указать: «Веб-интерфейс и доп. материалы — в проекте papa-app»; + - либо вынести общие документы в отдельную папку (например `Desktop/PAPA-docs`) и из обеих папок на неё ссылаться. + +Объединение в одну папку для загрузки/правок здесь не рекомендуется. + +--- + +## 3. Конкретные шаги (рекомендации, без автоматических изменений) + +1. **Определить единственную рабочую папку** + - Для десктопного приложения: **`/Users/.../Desktop/papa-yu`**. + - Все пути в инструкциях (README, docs, скрипты) вести относительно неё. + +2. **В README или docs папы-yu** + - Явно написать: «Проект ведётся из одной папки — papa-yu. Запуск, сборка и правки — только из её корня.» + - Указать при необходимости: «ТЗ и этапы — в папке папа-ю (кириллица) по пути …». + +3. **Скрипты запуска** + - Все скрипты (например `start-with-openai.sh`, будущий `Собрать и запустить PAPA-YU.command`) должны: + - находиться в `papa-yu/`; + - выполнять `cd` в корень `papa-yu` (например `cd "$(dirname "$0")"`); + - не ссылаться на «PAPA-YU» или «папа-ю» как на каталог с кодом. + +4. **Имя папки в системе** + - Для избежания путаницы с регистром и пробелами лучше везде использовать **`papa-yu`** (латиница, один регистр). Не создавать дубликат с именем «PAPA YU» или «PAPA-YU» для кода. + +5. **Документация из папа-ю** + - Если позже понадобится «всё в одном месте»: можно скопировать выбранные DOCX/PDF из `папа-ю` в `papa-yu/docs/specs/` (или аналогичную подпапку) и при желании обновить ссылки в README. Это уже будет решение по переносу файлов; в текущем документе достаточно понимать, что логически «одна папка» — это `papa-yu`, а `папа-ю` — внешний архив, связь с которым задаётся явной ссылкой в документации. + +--- + +## 4. Краткая сводка + +| Вопрос | Ответ | +|--------|--------| +| Какую папку считать «одной» для загрузки и правок? | **`papa-yu`** (латиница). | +| Где вносить изменения в код и конфиги? | Только в **`papa-yu`**. | +| Откуда запускать и собирать приложение? | Из корня **`papa-yu`**. | +| Что делать с папкой `папа-ю`? | Оставить как хранилище ТЗ; в `papa-yu` описать путь к ней в README/docs или (по желанию) добавить симлинк в `papa-yu/docs/`. | +| Нужно ли объединять с `papa-app`? | Нет; это другой продукт. При необходимости — только ссылка в документации. | +| «PAPA YU» как папка? | Отдельной папки с таким именем нет; это название приложения. Рабочая папка — **`papa-yu`**. | + +Все рекомендации выше можно выполнять вручную; автоматических изменений в файлы этот документ не вносит. diff --git a/env.openai.example b/env.openai.example new file mode 100644 index 0000000..9d2ec4e --- /dev/null +++ b/env.openai.example @@ -0,0 +1,33 @@ +# Скопируйте этот файл в .env.openai и подставьте свой ключ OpenAI. +# Команда: cp env.openai.example .env.openai +# Затем откройте .env.openai и замените your-openai-key-here на ваш ключ. + +PAPAYU_LLM_API_URL=https://api.openai.com/v1/chat/completions +PAPAYU_LLM_API_KEY=your-openai-key-here +PAPAYU_LLM_MODEL=gpt-4o-mini + +# Строгий JSON (OpenAI Structured Outputs): добавляет response_format с JSON Schema. +# Работает с OpenAI; Ollama и др. могут не поддерживать — не задавать или =0. +# PAPAYU_LLM_STRICT_JSON=1 + +# memory_patch: 0 (по умолчанию) — игнорировать; 1 — применять по whitelist. +# PAPAYU_MEMORY_AUTOPATCH=0 + +# EOL: keep (по умолчанию) — не менять; lf — нормализовать \r\n→\n, trailing newline. +# PAPAYU_NORMALIZE_EOL=lf + +# LLM: температура 0 (детерминизм), max_tokens 16384 (авто-кэп при input>80k → 4096). +# PAPAYU_LLM_TEMPERATURE=0 +# PAPAYU_LLM_MAX_TOKENS=16384 + +# Таймаут запроса к LLM (сек). +# PAPAYU_LLM_TIMEOUT_SEC=90 + +# Трассировка: PAPAYU_TRACE=1 → пишет в .papa-yu/traces/.json (без raw_content по умолчанию). +# PAPAYU_TRACE=1 +# PAPAYU_TRACE_RAW=1 — сохранять raw_content (с маскировкой sk-/Bearer) + +# Контекст-диета: max 8 файлов, 20k на файл, 120k total. +# PAPAYU_CONTEXT_MAX_FILES=8 +# PAPAYU_CONTEXT_MAX_FILE_CHARS=20000 +# PAPAYU_CONTEXT_MAX_TOTAL_CHARS=120000 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b1a47cf --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + PAPA YU + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..346f80b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2610 @@ +{ + "name": "papa-yu", + "version": "2.4.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "papa-yu", + "version": "2.4.3", + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "sharp": "^0.34.5", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz", + "integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz", + "integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.9.6", + "@tauri-apps/cli-darwin-x64": "2.9.6", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", + "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", + "@tauri-apps/cli-linux-arm64-musl": "2.9.6", + "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", + "@tauri-apps/cli-linux-x64-gnu": "2.9.6", + "@tauri-apps/cli-linux-x64-musl": "2.9.6", + "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", + "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", + "@tauri-apps/cli-win32-x64-msvc": "2.9.6" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz", + "integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz", + "integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz", + "integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz", + "integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz", + "integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz", + "integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz", + "integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz", + "integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz", + "integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz", + "integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz", + "integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", + "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90fefda --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "papa-yu", + "version": "2.4.4", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "tauri": "tauri", + "icons:export": "node scripts/export-icon.js" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "sharp": "^0.34.5", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } +} diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..11cdccb Binary files /dev/null and b/public/logo.png differ diff --git a/public/send-icon.png b/public/send-icon.png new file mode 100644 index 0000000..68936c3 Binary files /dev/null and b/public/send-icon.png differ diff --git a/scripts/export-icon.js b/scripts/export-icon.js new file mode 100644 index 0000000..72a0668 --- /dev/null +++ b/scripts/export-icon.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * Экспорт иконки: SVG → PNG 1024x1024 для сборки Tauri. + * Варианты: ImageMagick (convert/magick), иначе npm install sharp && node scripts/export-icon.js + */ +const path = require('path'); +const fs = require('fs'); +const { execSync } = require('child_process'); + +const src = path.join(__dirname, '../src-tauri/icons/icon.svg'); +const out = path.join(__dirname, '../src-tauri/icons/icon.png'); + +if (!fs.existsSync(src)) { + console.error('Не найден файл:', src); + process.exit(1); +} + +function tryImageMagick() { + try { + execSync('convert -version', { stdio: 'ignore' }); + execSync(`convert -background none -resize 1024x1024 "${src}" "${out}"`, { stdio: 'inherit' }); + return true; + } catch (_) {} + try { + execSync('magick -version', { stdio: 'ignore' }); + execSync(`magick convert -background none -resize 1024x1024 "${src}" "${out}"`, { stdio: 'inherit' }); + return true; + } catch (_) {} + return false; +} + +async function run() { + if (tryImageMagick()) { + console.log('Иконка экспортирована (ImageMagick):', out); + return; + } + try { + const sharp = require('sharp'); + await sharp(src) + .resize(1024, 1024) + .png() + .toFile(out); + console.log('Иконка экспортирована (sharp):', out); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + console.error('Установите sharp: npm install --save-dev sharp'); + console.error('Или экспортируйте вручную: откройте src-tauri/icons/icon.svg в браузере/редакторе и сохраните как PNG 1024×1024 в icon.png'); + } else { + console.error(e.message); + } + process.exit(1); + } +} + +run(); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..fc1f65d --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "papa-yu" +version = "2.4.4" +edition = "2021" +description = "PAPA YU — анализ и исправление проектов" + +[lib] +name = "papa_yu_lib" +crate-type = ["lib", "cdylib", "staticlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json"] } +walkdir = "2" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4", "serde"] } +jsonschema = "0.18" +sha2 = "0.10" + +[dev-dependencies] +tempfile = "3" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +strip = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..00e335d --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "identifier": "default", + "description": "Default capability for PAPA YU", + "windows": ["*"], + "permissions": [ + "core:default", + "core:path:default", + "shell:allow-open", + "dialog:allow-open" + ] +} diff --git a/src-tauri/config/llm_response_schema.json b/src-tauri/config/llm_response_schema.json new file mode 100644 index 0000000..6cbd1c2 --- /dev/null +++ b/src-tauri/config/llm_response_schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "x_schema_version": 1, + "oneOf": [ + { + "type": "array", + "items": { "$ref": "#/$defs/action" }, + "minItems": 0 + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "mode": { "type": "string", "enum": ["fix-plan", "apply"] }, + "actions": { + "type": "array", + "items": { "$ref": "#/$defs/action" } + }, + "proposed_changes": { + "type": "object", + "additionalProperties": true, + "properties": { + "actions": { + "type": "array", + "items": { "$ref": "#/$defs/action" } + } + } + }, + "summary": { "type": "string" }, + "questions": { "type": "array", "items": { "type": "string" } }, + "context_requests": { + "type": "array", + "items": { "$ref": "#/$defs/context_request" } + }, + "plan": { + "type": "array", + "items": { + "type": "object", + "properties": { "step": { "type": "string" }, "details": { "type": "string" } } + } + }, + "memory_patch": { "type": "object", "additionalProperties": true }, + "risks": { "type": "array", "items": { "type": "string" } } + } + } + ], + "$defs": { + "action": { + "type": "object", + "additionalProperties": true, + "required": ["kind", "path"], + "properties": { + "kind": { + "type": "string", + "enum": ["CREATE_FILE", "CREATE_DIR", "UPDATE_FILE", "DELETE_FILE", "DELETE_DIR"] + }, + "path": { "type": "string" }, + "content": { "type": "string" } + } + }, + "context_request": { + "type": "object", + "additionalProperties": true, + "required": ["type"], + "properties": { + "type": { "type": "string", "enum": ["read_file", "search", "logs", "env"] }, + "path": { "type": "string" }, + "start_line": { "type": "integer", "minimum": 1 }, + "end_line": { "type": "integer", "minimum": 1 }, + "query": { "type": "string" }, + "glob": { "type": "string" }, + "source": { "type": "string" }, + "last_n": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + } + } +} diff --git a/src-tauri/config/verify_allowlist.json b/src-tauri/config/verify_allowlist.json new file mode 100644 index 0000000..5413003 --- /dev/null +++ b/src-tauri/config/verify_allowlist.json @@ -0,0 +1,19 @@ +{ + "rust": [ + { "exe": "cargo", "args": ["check"], "name": "cargo check", "timeout_sec": 120 }, + { "exe": "cargo", "args": ["test", "--no-run"], "name": "cargo test --no-run", "timeout_sec": 180 }, + { "exe": "cargo", "args": ["clippy", "--", "-D", "warnings"], "name": "cargo clippy", "timeout_sec": 120 } + ], + "node": [ + { "exe": "npm", "args": ["run", "-s", "test"], "name": "npm test", "timeout_sec": 120 }, + { "exe": "npm", "args": ["run", "-s", "build"], "name": "npm run build", "timeout_sec": 180 }, + { "exe": "npm", "args": ["run", "-s", "lint"], "name": "npm run lint", "timeout_sec": 60 }, + { "exe": "npm", "args": ["run", "-s", "typecheck"], "name": "npm run typecheck", "timeout_sec": 60 }, + { "exe": "npx", "args": ["tsc", "--noEmit"], "name": "tsc --noEmit", "timeout_sec": 60 } + ], + "python": [ + { "exe": "python3", "args": ["-m", "compileall", ".", "-q"], "name": "python -m compileall", "timeout_sec": 60 }, + { "exe": "python3", "args": ["-m", "pytest", "--collect-only", "-q"], "name": "pytest --collect-only", "timeout_sec": 60 }, + { "exe": "python3", "args": ["-m", "mypy", "."], "name": "mypy", "timeout_sec": 120 } + ] +} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..35f90a7 --- /dev/null +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..9c53dad --- /dev/null +++ b/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"default":{"identifier":"default","description":"Default capability for PAPA YU","local":true,"windows":["*"],"permissions":["core:default","core:path:default","shell:allow-open","dialog:allow-open"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..fcf88e0 --- /dev/null +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2630 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "default": false, + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json new file mode 100644 index 0000000..fcf88e0 --- /dev/null +++ b/src-tauri/gen/schemas/macOS-schema.json @@ -0,0 +1,2630 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "default": false, + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/icons/README.md b/src-tauri/icons/README.md new file mode 100644 index 0000000..f26a55a --- /dev/null +++ b/src-tauri/icons/README.md @@ -0,0 +1,17 @@ +# Иконка приложения PAPA YU + +- **icon.svg** — исходная иконка (код/скобки + галочка «исправлено», синий фон, оранжевый акцент). +- **icon.png** — используется в сборке Tauri (1024×1024). + +Чтобы пересобрать PNG из SVG (после изменения иконки): + +```bash +# из корня проекта papa-yu +npm run icons:export +``` + +Требуется один из вариантов: +- **ImageMagick:** `brew install imagemagick` (на macOS) +- **sharp:** `npm install --save-dev sharp` + +Либо откройте `icon.svg` в браузере или редакторе (Figma, Inkscape) и экспортируйте как PNG 1024×1024 в `icon.png`. diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..0ea057a Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/icon.svg b/src-tauri/icons/icon.svg new file mode 100644 index 0000000..05d9234 --- /dev/null +++ b/src-tauri/icons/icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/src/commands/agentic_run.rs b/src-tauri/src/commands/agentic_run.rs new file mode 100644 index 0000000..ab739f5 --- /dev/null +++ b/src-tauri/src/commands/agentic_run.rs @@ -0,0 +1,312 @@ +//! v2.4: Agentic Loop — analyze → plan → preview → apply → verify → auto-rollback → retry. + +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use tauri::{Emitter, Manager, Window}; + +use crate::commands::{analyze_project, apply_actions_tx, generate_actions_from_report, get_project_profile, preview_actions, undo_last_tx}; +use crate::types::{ + Action, ActionKind, AgenticRunRequest, AgenticRunResult, AttemptResult, + ApplyOptions, ApplyPayload, VerifyResult, +}; +use crate::verify::verify_project; + +const AGENTIC_PROGRESS: &str = "agentic_progress"; + +#[derive(Clone, Serialize, Deserialize)] +pub struct AgenticProgressPayload { + pub stage: String, + pub message: String, + pub attempt: u8, +} + +fn emit_progress(window: &Window, stage: &str, message: &str, attempt: u8) { + let _ = window.emit( + AGENTIC_PROGRESS, + AgenticProgressPayload { + stage: stage.to_string(), + message: message.to_string(), + attempt, + }, + ); +} + +fn has_readme(root: &Path) -> bool { + ["README.md", "README.MD", "README.txt", "README"] + .iter() + .any(|f| root.join(f).exists()) +} + +fn has_gitignore(root: &Path) -> bool { + root.join(".gitignore").exists() +} + +fn has_src(root: &Path) -> bool { + root.join("src").is_dir() +} + +fn has_tests(root: &Path) -> bool { + root.join("tests").is_dir() +} + +fn has_editorconfig(root: &Path) -> bool { + root.join(".editorconfig").exists() +} + +/// v2.4.0: эвристический план (без LLM). README, .gitignore, tests/README.md, .editorconfig. +fn build_plan( + path: &str, + _goal: &str, + max_actions: u16, +) -> (String, Vec) { + let root = Path::new(path); + let mut actions: Vec = vec![]; + let mut plan_parts: Vec = vec![]; + + if !has_readme(root) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: "README.md".to_string(), + content: Some( + "# Project\n\n## Описание\n\nКратко опишите проект.\n\n## Запуск\n\n- dev: ...\n- build: ...\n\n## Структура\n\n- src/\n- tests/\n".into(), + ), + }); + plan_parts.push("README.md".into()); + } + + if !has_gitignore(root) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: ".gitignore".to_string(), + content: Some( + "node_modules/\ndist/\nbuild/\n.next/\ncoverage/\n.env\n.env.*\n.DS_Store\n.target/\n".into(), + ), + }); + plan_parts.push(".gitignore".into()); + } + + if has_src(root) && !has_tests(root) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: "tests/README.md".to_string(), + content: Some("# Тесты\n\nДобавьте unit- и интеграционные тесты.\n".into()), + }); + plan_parts.push("tests/README.md".into()); + } + + if !has_editorconfig(root) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: ".editorconfig".to_string(), + content: Some( + "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\n".into(), + ), + }); + plan_parts.push(".editorconfig".into()); + } + + let n = max_actions as usize; + if actions.len() > n { + actions.truncate(n); + } + + let plan = if plan_parts.is_empty() { + "Нет безопасных правок для применения.".to_string() + } else { + format!("План: добавить {}", plan_parts.join(", ")) + }; + + (plan, actions) +} + +#[tauri::command] +pub async fn agentic_run(window: Window, payload: AgenticRunRequest) -> AgenticRunResult { + let path = payload.path.clone(); + let user_goal = payload.goal.clone(); + let constraints = payload.constraints.clone(); + let app = window.app_handle(); + + let profile = match get_project_profile(window.clone(), path.clone()).await { + Ok(p) => p, + Err(e) => { + return AgenticRunResult { + ok: false, + attempts: vec![], + final_summary: format!("Ошибка профиля: {}", e), + error: Some(e.clone()), + error_code: Some("PROFILE_ERROR".into()), + }; + } + }; + + let max_attempts = profile.max_attempts.max(1); + let max_actions = (profile.limits.max_actions_per_tx as u16).max(1); + let goal = profile.goal_template.replace("{goal}", &user_goal); + let _safe_mode = profile.safe_mode; + + let mut attempts: Vec = vec![]; + + for attempt in 1..=max_attempts { + let attempt_u8 = attempt.min(255) as u8; + emit_progress(&window, "analyze", "Сканирую проект…", attempt_u8); + + let report = match analyze_project(vec![path.clone()], None) { + Ok(r) => r, + Err(e) => { + emit_progress(&window, "failed", "Ошибка анализа.", attempt_u8); + return AgenticRunResult { + ok: false, + attempts, + final_summary: format!("Ошибка анализа: {}", e), + error: Some(e), + error_code: Some("ANALYZE_FAILED".into()), + }; + } + }; + + emit_progress(&window, "plan", "Составляю план исправлений…", attempt_u8); + let gen = generate_actions_from_report( + path.clone(), + report.clone(), + "safe_create_only".to_string(), + ) + .await; + let (plan, actions) = if gen.ok && !gen.actions.is_empty() { + let n = max_actions as usize; + let mut a = gen.actions; + if a.len() > n { + a.truncate(n); + } + ( + format!("План из отчёта: {} действий.", a.len()), + a, + ) + } else { + build_plan(&path, &goal, max_actions) + }; + + if actions.is_empty() { + emit_progress(&window, "done", "Готово.", attempt_u8); + return AgenticRunResult { + ok: true, + attempts, + final_summary: plan.clone(), + error: None, + error_code: None, + }; + } + + emit_progress(&window, "preview", "Показываю, что изменится…", attempt_u8); + let preview = match preview_actions(ApplyPayload { + root_path: path.clone(), + actions: actions.clone(), + auto_check: None, + label: None, + user_confirmed: false, + }) { + Ok(p) => p, + Err(e) => { + emit_progress(&window, "failed", "Ошибка предпросмотра.", attempt_u8); + return AgenticRunResult { + ok: false, + attempts, + final_summary: format!("Ошибка предпросмотра: {}", e), + error: Some(e), + error_code: Some("PREVIEW_FAILED".into()), + }; + } + }; + + emit_progress(&window, "apply", "Применяю изменения…", attempt_u8); + let apply_result = apply_actions_tx( + app.clone(), + path.clone(), + actions.clone(), + ApplyOptions { + auto_check: false, + user_confirmed: true, + }, + ) + .await; + + if !apply_result.ok { + emit_progress(&window, "failed", "Не удалось безопасно применить изменения.", attempt_u8); + let err = apply_result.error.clone(); + let code = apply_result.error_code.clone(); + attempts.push(AttemptResult { + attempt: attempt_u8, + plan: plan.clone(), + actions: actions.clone(), + preview, + apply: apply_result, + verify: VerifyResult { + ok: false, + checks: vec![], + error: None, + error_code: None, + }, + }); + return AgenticRunResult { + ok: false, + attempts, + final_summary: "Apply не выполнен.".to_string(), + error: err, + error_code: code, + }; + } + + let verify = if constraints.auto_check { + emit_progress(&window, "verify", "Проверяю сборку/типы…", attempt_u8); + let v = verify_project(&path); + if !v.ok { + emit_progress(&window, "revert", "Обнаружены ошибки. Откатываю изменения…", attempt_u8); + let _ = undo_last_tx(app.clone(), path.clone()).await; + attempts.push(AttemptResult { + attempt: attempt_u8, + plan: plan.clone(), + actions: actions.clone(), + preview, + apply: apply_result, + verify: v, + }); + continue; + } + v + } else { + VerifyResult { + ok: true, + checks: vec![], + error: None, + error_code: None, + } + }; + + attempts.push(AttemptResult { + attempt: attempt_u8, + plan: plan.clone(), + actions: actions.clone(), + preview, + apply: apply_result, + verify: verify.clone(), + }); + + emit_progress(&window, "done", "Готово.", attempt_u8); + return AgenticRunResult { + ok: true, + attempts, + final_summary: plan, + error: None, + error_code: None, + }; + } + + emit_progress(&window, "failed", "Не удалось безопасно применить изменения.", max_attempts.min(255) as u8); + AgenticRunResult { + ok: false, + attempts, + final_summary: "Превышено число попыток. Изменения откачены.".to_string(), + error: Some("max_attempts exceeded".into()), + error_code: Some("MAX_ATTEMPTS_EXCEEDED".into()), + } +} diff --git a/src-tauri/src/commands/analyze_project.rs b/src-tauri/src/commands/analyze_project.rs new file mode 100644 index 0000000..a2d7900 --- /dev/null +++ b/src-tauri/src/commands/analyze_project.rs @@ -0,0 +1,246 @@ +use crate::types::{Action, ActionGroup, ActionKind, AnalyzeReport, Finding, FixPack, ProjectSignal}; +use std::path::Path; + +pub fn analyze_project(paths: Vec, attached_files: Option>) -> Result { + let path = paths.first().cloned().unwrap_or_else(|| ".".to_string()); + let root = Path::new(&path); + if !root.is_dir() { + return Ok(AnalyzeReport { + path: path.clone(), + narrative: format!("Папка не найдена: {}", path), + findings: vec![], + recommendations: vec![], + actions: vec![], + action_groups: vec![], + fix_packs: vec![], + recommended_pack_ids: vec![], + attached_files, + }); + } + + let has_readme = root.join("README.md").is_file(); + let has_gitignore = root.join(".gitignore").is_file(); + let has_env = root.join(".env").is_file(); + let has_src = root.join("src").is_dir(); + let has_tests = root.join("tests").is_dir(); + let has_package = root.join("package.json").is_file(); + let has_cargo = root.join("Cargo.toml").is_file(); + + let mut findings = Vec::new(); + let recommendations = Vec::new(); + let action_groups = build_action_groups(root, has_readme, has_gitignore, has_src, has_tests, has_package, has_cargo); + let mut actions: Vec = action_groups.iter().flat_map(|g| g.actions.clone()).collect(); + + if !has_readme { + findings.push(Finding { + title: "Нет README.md".to_string(), + details: "Рекомендуется добавить описание проекта.".to_string(), + path: Some(path.clone()), + }); + } + if !has_gitignore { + findings.push(Finding { + title: "Нет .gitignore".to_string(), + details: "Рекомендуется добавить .gitignore для типа проекта.".to_string(), + path: Some(path.clone()), + }); + } + if has_env { + findings.push(Finding { + title: "Найден .env".to_string(), + details: "Рекомендуется создать .env.example и не коммитить .env.".to_string(), + path: Some(path.clone()), + }); + actions.push(Action { + kind: ActionKind::CreateFile, + path: ".env.example".to_string(), + content: Some("# Copy to .env and fill\n".to_string()), + }); + } + if has_src && !has_tests { + findings.push(Finding { + title: "Нет папки tests/".to_string(), + details: "Рекомендуется добавить tests/ и README в ней.".to_string(), + path: Some(path.clone()), + }); + } + + let signals = build_signals_from_findings(&findings); + let (fix_packs, recommended_pack_ids) = build_fix_packs(&action_groups, &signals); + + let narrative = format!( + "Проанализировано: {}. Найдено проблем: {}, рекомендаций: {}, действий: {}.", + path, + findings.len(), + recommendations.len(), + actions.len() + ); + + Ok(AnalyzeReport { + path, + narrative, + findings, + recommendations, + actions, + action_groups, + fix_packs, + recommended_pack_ids, + attached_files, + }) +} + +fn build_action_groups( + _path: &Path, + has_readme: bool, + has_gitignore: bool, + has_src: bool, + has_tests: bool, + has_package: bool, + has_cargo: bool, +) -> Vec { + let mut groups: Vec = vec![]; + + if !has_readme { + groups.push(ActionGroup { + id: "readme".into(), + title: "Добавить README".into(), + description: "Создаст README.md с базовой структурой.".into(), + actions: vec![Action { + kind: ActionKind::CreateFile, + path: "README.md".into(), + content: Some("# Project\n\n## Overview\n\n## How to run\n\n## Tests\n\n".into()), + }], + }); + } + + if !has_gitignore { + let content = if has_package { + "node_modules/\n.env\n.DS_Store\ndist/\n*.log\n" + } else if has_cargo { + "target/\n.env\n.DS_Store\nCargo.lock\n" + } else { + ".env\n.DS_Store\n__pycache__/\n*.pyc\n.venv/\n" + }; + groups.push(ActionGroup { + id: "gitignore".into(), + title: "Добавить .gitignore".into(), + description: "Создаст .gitignore со стандартными исключениями.".into(), + actions: vec![Action { + kind: ActionKind::CreateFile, + path: ".gitignore".into(), + content: Some(content.to_string()), + }], + }); + } + + if !has_tests && has_src { + groups.push(ActionGroup { + id: "tests".into(), + title: "Добавить tests/".into(), + description: "Создаст папку tests/ и README для тестов.".into(), + actions: vec![ + Action { + kind: ActionKind::CreateDir, + path: "tests".into(), + content: None, + }, + Action { + kind: ActionKind::CreateFile, + path: "tests/README.md".into(), + content: Some("# Tests\n\nAdd tests here.\n".into()), + }, + ], + }); + } + + groups +} + +fn build_signals_from_findings(findings: &[Finding]) -> Vec { + let mut signals: Vec = vec![]; + for f in findings { + if f.title.contains("gitignore") { + signals.push(ProjectSignal { + category: "security".into(), + level: "high".into(), + }); + } + if f.title.contains("README") { + signals.push(ProjectSignal { + category: "quality".into(), + level: "warn".into(), + }); + } + if f.title.contains("tests") || f.details.to_lowercase().contains("тест") { + signals.push(ProjectSignal { + category: "quality".into(), + level: "warn".into(), + }); + } + } + signals +} + +fn build_fix_packs(action_groups: &[ActionGroup], signals: &[ProjectSignal]) -> (Vec, Vec) { + let mut security: Vec = vec![]; + let mut quality: Vec = vec![]; + let structure: Vec = vec![]; + + for g in action_groups { + match g.id.as_str() { + "gitignore" => security.push(g.id.clone()), + "readme" => quality.push(g.id.clone()), + "tests" => quality.push(g.id.clone()), + _ => {} + } + } + + let mut recommended: Vec = vec![]; + let has_high_security = signals + .iter() + .any(|s| s.category == "security" && (s.level == "high" || s.level == "critical")); + let has_quality_issues = signals + .iter() + .any(|s| s.category == "quality" && (s.level == "warn" || s.level == "high")); + let has_structure_issues = signals + .iter() + .any(|s| s.category == "structure" && (s.level == "warn" || s.level == "high")); + + if has_high_security && !security.is_empty() { + recommended.push("security".into()); + } + if has_quality_issues && !quality.is_empty() { + recommended.push("quality".into()); + } + if has_structure_issues && !structure.is_empty() { + recommended.push("structure".into()); + } + + if recommended.is_empty() && !quality.is_empty() { + recommended.push("quality".into()); + } + + let packs = vec![ + FixPack { + id: "security".into(), + title: "Безопасность".into(), + description: "Снижает риск утечки секретов и мусора в репозитории.".into(), + group_ids: security, + }, + FixPack { + id: "quality".into(), + title: "Качество".into(), + description: "Базовые улучшения читаемости и проверяемости проекта.".into(), + group_ids: quality, + }, + FixPack { + id: "structure".into(), + title: "Структура".into(), + description: "Наводит порядок в структуре проекта и соглашениях.".into(), + group_ids: structure, + }, + ]; + + (packs, recommended) +} + diff --git a/src-tauri/src/commands/apply_actions.rs b/src-tauri/src/commands/apply_actions.rs new file mode 100644 index 0000000..41ffb78 --- /dev/null +++ b/src-tauri/src/commands/apply_actions.rs @@ -0,0 +1,214 @@ +use std::path::Path; +use tauri::AppHandle; + +use crate::commands::auto_check::auto_check; +use crate::tx::{ + apply_one_action, clear_redo, collect_rel_paths, ensure_history, new_tx_id, + preflight_actions, push_undo, rollback_tx, snapshot_before, sort_actions_for_apply, write_manifest, +}; +use crate::types::{ApplyPayload, ApplyResult, TxManifest}; + +pub const AUTO_CHECK_FAILED_REVERTED: &str = "AUTO_CHECK_FAILED_REVERTED"; +#[allow(dead_code)] +pub const APPLY_FAILED_REVERTED: &str = "APPLY_FAILED_REVERTED"; +/// v2.3.3: apply failed at step N, rolled back applied steps +pub const AUTO_ROLLBACK_DONE: &str = "AUTO_ROLLBACK_DONE"; + +pub fn apply_actions(app: AppHandle, payload: ApplyPayload) -> ApplyResult { + let root = match Path::new(&payload.root_path).canonicalize() { + Ok(p) => p, + Err(_) => Path::new(&payload.root_path).to_path_buf(), + }; + if !root.exists() || !root.is_dir() { + return ApplyResult { + ok: false, + tx_id: None, + applied_count: None, + failed_at: None, + error: Some("path invalid".into()), + error_code: Some("PATH_INVALID".into()), + }; + } + + if !payload.user_confirmed { + return ApplyResult { + ok: false, + tx_id: None, + applied_count: None, + failed_at: None, + error: Some("confirmation required".into()), + error_code: Some("CONFIRM_REQUIRED".into()), + }; + } + + if ensure_history(&app).is_err() { + return ApplyResult { + ok: false, + tx_id: None, + applied_count: None, + failed_at: None, + error: Some("history init failed".into()), + error_code: Some("HISTORY_INIT_FAILED".into()), + }; + } + + if payload.actions.is_empty() { + return ApplyResult { + ok: true, + tx_id: None, + applied_count: Some(0), + failed_at: None, + error: None, + error_code: None, + }; + } + + if let Err((msg, code)) = preflight_actions(&root, &payload.actions) { + return ApplyResult { + ok: false, + tx_id: None, + applied_count: None, + failed_at: None, + error: Some(msg), + error_code: Some(code), + }; + } + + let tx_id = new_tx_id(); + let rel_paths = collect_rel_paths(&payload.actions); + let touched = match snapshot_before(&app, &tx_id, &root, &rel_paths) { + Ok(t) => t, + Err(e) => { + return ApplyResult { + ok: false, + tx_id: Some(tx_id.clone()), + applied_count: None, + failed_at: None, + error: Some(e), + error_code: Some("SNAPSHOT_FAILED".into()), + }; + } + }; + + let mut manifest = TxManifest { + tx_id: tx_id.clone(), + root_path: payload.root_path.clone(), + created_at: chrono::Utc::now().to_rfc3339(), + label: payload.label.clone(), + status: "pending".into(), + applied_actions: payload.actions.clone(), + touched: touched.clone(), + auto_check: payload.auto_check.unwrap_or(false), + snapshot_items: None, + }; + + if let Err(e) = write_manifest(&app, &manifest) { + return ApplyResult { + ok: false, + tx_id: Some(tx_id), + applied_count: None, + failed_at: None, + error: Some(e.to_string()), + error_code: Some("MANIFEST_WRITE_FAILED".into()), + }; + } + + // v2.4.2: guard — запрет lock/бинарников/не-текстовых + for action in &payload.actions { + let rel = action.path.as_str(); + if is_protected_file(rel) || !is_text_allowed(rel) { + return ApplyResult { + ok: false, + tx_id: Some(tx_id.clone()), + applied_count: None, + failed_at: None, + error: Some(format!("protected or non-text file: {}", rel)), + error_code: Some("PROTECTED_PATH".into()), + }; + } + } + + // v2.3.3: apply one-by-one; on first failure rollback and return AUTO_ROLLBACK_DONE + // Порядок применения: CREATE_DIR → CREATE/UPDATE → DELETE_FILE → DELETE_DIR + let mut sorted_actions = payload.actions.clone(); + sort_actions_for_apply(&mut sorted_actions); + for (i, action) in sorted_actions.iter().enumerate() { + if let Err(e) = apply_one_action(&root, action) { + let _ = rollback_tx(&app, &tx_id); + manifest.status = "rolled_back".into(); + let _ = write_manifest(&app, &manifest); + return ApplyResult { + ok: false, + tx_id: Some(tx_id.clone()), + applied_count: Some(i), + failed_at: Some(i), + error: Some(format!("apply failed, rolled back: {}", e)), + error_code: Some(AUTO_ROLLBACK_DONE.into()), + }; + } + } + + if payload.auto_check.unwrap_or(false) { + if let Err(_) = auto_check(&root) { + let _ = rollback_tx(&app, &tx_id); + return ApplyResult { + ok: false, + tx_id: Some(tx_id), + applied_count: Some(payload.actions.len()), + failed_at: None, + error: Some("Ошибки после изменений. Откат выполнен.".into()), + error_code: Some(AUTO_CHECK_FAILED_REVERTED.into()), + }; + } + } + + manifest.status = "committed".into(); + let _ = write_manifest(&app, &manifest); + let _ = push_undo(&app, tx_id.clone()); + let _ = clear_redo(&app); + + ApplyResult { + ok: true, + tx_id: Some(tx_id), + applied_count: Some(payload.actions.len()), + failed_at: None, + error: None, + error_code: None, + } +} + +fn is_protected_file(p: &str) -> bool { + let lower = p.to_lowercase().replace('\\', "/"); + if lower == ".env" || lower.ends_with("/.env") { return true; } + if lower.ends_with(".pem") || lower.ends_with(".key") || lower.ends_with(".p12") { return true; } + if lower.contains("id_rsa") { return true; } + if lower.contains("/secrets/") || lower.starts_with("secrets/") { return true; } + if lower.ends_with("cargo.lock") { return true; } + if lower.ends_with("package-lock.json") { return true; } + if lower.ends_with("pnpm-lock.yaml") { return true; } + if lower.ends_with("yarn.lock") { return true; } + if lower.ends_with("composer.lock") { return true; } + if lower.ends_with("poetry.lock") { return true; } + if lower.ends_with("pipfile.lock") { return true; } + let bin_ext = [ + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", + ".pdf", ".zip", ".7z", ".rar", ".dmg", ".pkg", + ".exe", ".dll", ".so", ".dylib", ".bin", + ".mp3", ".mp4", ".mov", ".avi", + ".wasm", ".class", + ]; + for ext in bin_ext { + if lower.ends_with(ext) { return true; } + } + false +} + +fn is_text_allowed(p: &str) -> bool { + let lower = p.to_lowercase(); + let ok_ext = [ + ".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".toml", ".yaml", ".yml", + ".rs", ".py", ".go", ".java", ".kt", ".c", ".cpp", ".h", ".hpp", + ".css", ".scss", ".html", ".env", ".gitignore", ".editorconfig", + ]; + ok_ext.iter().any(|e| lower.ends_with(e)) || !lower.contains('.') +} diff --git a/src-tauri/src/commands/apply_actions_tx.rs b/src-tauri/src/commands/apply_actions_tx.rs new file mode 100644 index 0000000..6a44339 --- /dev/null +++ b/src-tauri/src/commands/apply_actions_tx.rs @@ -0,0 +1,521 @@ +//! v3.1: транзакция — snapshot + apply + autocheck + autorollback (history/tx, history/snapshots) + +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use serde_json::json; +use tauri::{AppHandle, Emitter, Manager}; +use uuid::Uuid; + +use crate::commands::get_project_profile::get_project_limits; +use crate::tx::{normalize_content_for_write, safe_join, sort_actions_for_apply}; +use crate::types::{Action, ActionKind, ApplyOptions, ApplyTxResult, CheckStageResult}; + +const PROGRESS_EVENT: &str = "analyze_progress"; + +fn clip(s: String, n: usize) -> String { + if s.len() <= n { + s + } else { + format!("{}…", &s[..n]) + } +} + +fn emit_progress(app: &AppHandle, msg: &str) { + let _ = app.emit(PROGRESS_EVENT, msg); +} + +fn write_tx_record( + app: &AppHandle, + tx_id: &str, + record: &serde_json::Value, +) -> Result<(), String> { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let tx_dir = dir.join("history").join("tx"); + fs::create_dir_all(&tx_dir).map_err(|e| e.to_string())?; + let p = tx_dir.join(format!("{tx_id}.json")); + let bytes = + serde_json::to_vec_pretty(record).map_err(|e| e.to_string())?; + fs::write(&p, bytes).map_err(|e| e.to_string()) +} + +fn copy_dir_recursive( + src: &Path, + dst: &Path, + exclude: &[&str], +) -> Result<(), String> { + if exclude + .iter() + .any(|x| src.file_name().map(|n| n == *x).unwrap_or(false)) + { + return Ok(()); + } + fs::create_dir_all(dst).map_err(|e| e.to_string())?; + for entry in fs::read_dir(src).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let p = entry.path(); + let name = entry.file_name(); + let dstp = dst.join(name); + let ft = entry.file_type().map_err(|e| e.to_string())?; + if ft.is_dir() { + copy_dir_recursive(&p, &dstp, exclude)?; + } else if ft.is_file() { + fs::copy(&p, &dstp).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +fn snapshot_project( + app: &AppHandle, + project_root: &Path, + tx_id: &str, +) -> Result { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let snap_dir = dir.join("history").join("snapshots").join(tx_id); + if snap_dir.exists() { + fs::remove_dir_all(&snap_dir).map_err(|e| e.to_string())?; + } + fs::create_dir_all(&snap_dir).map_err(|e| e.to_string())?; + + let exclude = [ + ".git", "node_modules", "dist", "build", ".next", "target", ".cache", "coverage", + ]; + copy_dir_recursive(project_root, &snap_dir, &exclude)?; + Ok(snap_dir) +} + +fn restore_snapshot(project_root: &Path, snap_dir: &Path) -> Result<(), String> { + let exclude = [ + ".git", "node_modules", "dist", "build", ".next", "target", ".cache", "coverage", + ]; + + for entry in fs::read_dir(project_root).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let p = entry.path(); + let name = entry.file_name(); + if exclude + .iter() + .any(|x| name.to_string_lossy().as_ref() == *x) + { + continue; + } + if p.is_dir() { + fs::remove_dir_all(&p).map_err(|e| e.to_string())?; + } else { + fs::remove_file(&p).map_err(|e| e.to_string())?; + } + } + + copy_dir_recursive(snap_dir, project_root, &[])?; + Ok(()) +} + +fn apply_one_action(root: &Path, action: &Action) -> Result<(), String> { + let p = safe_join(root, &action.path)?; + match action.kind { + ActionKind::CreateFile | ActionKind::UpdateFile => { + let content = action.content.as_deref().unwrap_or(""); + let normalized = normalize_content_for_write(content, &p); + if let Some(parent) = p.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::write(&p, normalized.as_bytes()).map_err(|e| e.to_string())?; + Ok(()) + } + ActionKind::DeleteFile => { + if p.exists() { + fs::remove_file(&p).map_err(|e| e.to_string())?; + } + Ok(()) + } + ActionKind::CreateDir => { + fs::create_dir_all(&p).map_err(|e| e.to_string()) + } + ActionKind::DeleteDir => { + if p.exists() { + fs::remove_dir_all(&p).map_err(|e| e.to_string())?; + } + Ok(()) + } + } +} + +fn run_cmd_allowlisted( + cwd: &Path, + exe: &str, + args: &[&str], + timeout: Duration, +) -> Result { + let start = Instant::now(); + let mut cmd = std::process::Command::new(exe); + cmd.current_dir(cwd); + cmd.args(args); + cmd.env("CI", "1"); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| e.to_string())?; + loop { + if start.elapsed() > timeout { + let _ = child.kill(); + return Err("TIMEOUT".into()); + } + match child.try_wait().map_err(|e| e.to_string())? { + Some(_status) => { + let out = child.wait_with_output().map_err(|e| e.to_string())?; + let mut text = String::new(); + text.push_str(&String::from_utf8_lossy(&out.stdout)); + text.push_str(&String::from_utf8_lossy(&out.stderr)); + let text = clip(text, 20_000); + if out.status.success() { + return Ok(text); + } + return Err(text); + } + None => std::thread::sleep(Duration::from_millis(100)), + } + } +} + +fn auto_check(project_root: &Path, timeout_sec: u32) -> Vec { + let mut res: Vec = vec![]; + let timeout = Duration::from_secs(timeout_sec as u64); + + let cargo = project_root.join("Cargo.toml").exists(); + let pkg = project_root.join("package.json").exists(); + + if cargo { + match run_cmd_allowlisted(project_root, "cargo", &["check"], timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "verify".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "verify".into(), + ok: false, + output: out, + }), + } + } else if pkg { + match run_cmd_allowlisted(project_root, "npm", &["run", "-s", "typecheck"], timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "verify".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "verify".into(), + ok: false, + output: out, + }), + } + } + + if pkg { + let build_timeout = Duration::from_secs((timeout_sec as u64).max(120)); + match run_cmd_allowlisted(project_root, "npm", &["run", "-s", "build"], build_timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "build".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "build".into(), + ok: false, + output: out, + }), + } + } else if cargo { + let build_timeout = Duration::from_secs((timeout_sec as u64).max(120)); + match run_cmd_allowlisted(project_root, "cargo", &["build"], build_timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "build".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "build".into(), + ok: false, + output: out, + }), + } + } + + if pkg { + match run_cmd_allowlisted(project_root, "npm", &["test"], timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "smoke".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "smoke".into(), + ok: false, + output: out, + }), + } + } else if cargo { + match run_cmd_allowlisted(project_root, "cargo", &["test"], timeout) { + Ok(out) => res.push(CheckStageResult { + stage: "smoke".into(), + ok: true, + output: out, + }), + Err(out) => res.push(CheckStageResult { + stage: "smoke".into(), + ok: false, + output: out, + }), + } + } + + res +} + +#[tauri::command] +pub async fn apply_actions_tx( + app: AppHandle, + path: String, + actions: Vec, + options: ApplyOptions, +) -> ApplyTxResult { + let root = PathBuf::from(&path); + if !root.exists() || !root.is_dir() { + return ApplyTxResult { + ok: false, + tx_id: None, + applied: false, + rolled_back: false, + checks: vec![], + error: Some("path not found".into()), + error_code: Some("PATH_NOT_FOUND".into()), + }; + } + + if !options.user_confirmed { + return ApplyTxResult { + ok: false, + tx_id: None, + applied: false, + rolled_back: false, + checks: vec![], + error: Some("confirmation required".into()), + error_code: Some("CONFIRM_REQUIRED".into()), + }; + } + + let limits = get_project_limits(&root); + if actions.len() > limits.max_actions_per_tx as usize { + return ApplyTxResult { + ok: false, + tx_id: None, + applied: false, + rolled_back: false, + checks: vec![], + error: Some(format!( + "too many actions: {} > {}", + actions.len(), + limits.max_actions_per_tx + )), + error_code: Some("TOO_MANY_ACTIONS".into()), + }; + } + + for a in &actions { + let rel = a.path.as_str(); + if is_protected_file(rel) || !is_text_allowed(rel) { + return ApplyTxResult { + ok: false, + tx_id: None, + applied: false, + rolled_back: false, + checks: vec![], + error: Some(format!("protected or non-text file: {}", rel)), + error_code: Some("PROTECTED_PATH".into()), + }; + } + } + + let tx_id = Uuid::new_v4().to_string(); + + emit_progress(&app, "Сохраняю точку отката…"); + let snap_dir = match snapshot_project(&app, &root, &tx_id) { + Ok(p) => p, + Err(e) => { + return ApplyTxResult { + ok: false, + tx_id: Some(tx_id), + applied: false, + rolled_back: false, + checks: vec![], + error: Some(e), + error_code: Some("SNAPSHOT_FAILED".into()), + }; + } + }; + + emit_progress(&app, "Применяю изменения…"); + let mut actions = actions; + sort_actions_for_apply(&mut actions); + for a in &actions { + if let Err(e) = apply_one_action(&root, a) { + let _ = restore_snapshot(&root, &snap_dir); + eprintln!("[APPLY_ROLLBACK] tx_id={} path={} reason={}", tx_id, path, e); + return ApplyTxResult { + ok: false, + tx_id: Some(tx_id.clone()), + applied: false, + rolled_back: true, + checks: vec![], + error: Some(e), + error_code: Some("APPLY_FAILED_ROLLED_BACK".into()), + }; + } + } + + let mut checks: Vec = vec![]; + if options.auto_check { + emit_progress(&app, "Проверяю типы…"); + checks = auto_check(&root, limits.timeout_sec); + + let any_fail = checks.iter().any(|c| !c.ok); + if any_fail { + emit_progress(&app, "Обнаружены ошибки. Откатываю изменения…"); + let _ = restore_snapshot(&root, &snap_dir); + eprintln!("[APPLY_ROLLBACK] tx_id={} path={} reason=autoCheck_failed", tx_id, path); + + let record = json!({ + "txId": tx_id, + "path": path, + "rolledBack": true, + "checks": checks, + }); + let _ = write_tx_record(&app, &tx_id, &record); + + return ApplyTxResult { + ok: false, + tx_id: Some(tx_id), + applied: true, + rolled_back: true, + checks, + error: Some("autoCheck failed — rolled back".into()), + error_code: Some("AUTO_CHECK_FAILED_ROLLED_BACK".into()), + }; + } + } + + let record = json!({ + "txId": tx_id, + "path": path, + "rolledBack": false, + "checks": checks, + }); + let _ = write_tx_record(&app, &tx_id, &record); + + eprintln!("[APPLY_SUCCESS] tx_id={} path={} actions={}", tx_id, path, actions.len()); + + ApplyTxResult { + ok: true, + tx_id: Some(tx_id), + applied: true, + rolled_back: false, + checks, + error: None, + error_code: None, + } +} + +fn is_protected_file(p: &str) -> bool { + let lower = p.to_lowercase().replace('\\', "/"); + // Секреты и ключи (denylist) + if lower == ".env" || lower.ends_with("/.env") { return true; } + if lower.ends_with(".pem") || lower.ends_with(".key") || lower.ends_with(".p12") { return true; } + if lower.contains("id_rsa") { return true; } + if lower.contains("/secrets/") || lower.starts_with("secrets/") { return true; } + // Lock-файлы + if lower.ends_with("cargo.lock") { return true; } + if lower.ends_with("package-lock.json") { return true; } + if lower.ends_with("pnpm-lock.yaml") { return true; } + if lower.ends_with("yarn.lock") { return true; } + if lower.ends_with("composer.lock") { return true; } + if lower.ends_with("poetry.lock") { return true; } + if lower.ends_with("pipfile.lock") { return true; } + let bin_ext = [ + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", + ".pdf", ".zip", ".7z", ".rar", ".dmg", ".pkg", + ".exe", ".dll", ".so", ".dylib", ".bin", + ".mp3", ".mp4", ".mov", ".avi", + ".wasm", ".class", + ]; + for ext in bin_ext { + if lower.ends_with(ext) { return true; } + } + false +} + +fn is_text_allowed(p: &str) -> bool { + let lower = p.to_lowercase(); + let ok_ext = [ + ".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".toml", ".yaml", ".yml", + ".rs", ".py", ".go", ".java", ".kt", ".c", ".cpp", ".h", ".hpp", + ".css", ".scss", ".html", ".env", ".gitignore", ".editorconfig", + ]; + ok_ext.iter().any(|e| lower.ends_with(e)) || !lower.contains('.') +} + +#[cfg(test)] +mod tests { + use super::{is_protected_file, is_text_allowed}; + + #[test] + fn test_is_protected_file_secrets() { + assert!(is_protected_file(".env")); + assert!(is_protected_file("config/.env")); + assert!(is_protected_file("key.pem")); + assert!(is_protected_file("id_rsa")); + assert!(is_protected_file(".ssh/id_rsa")); + assert!(is_protected_file("secrets/secret.json")); + } + + #[test] + fn test_is_protected_file_lock_and_binary() { + assert!(is_protected_file("Cargo.lock")); + assert!(is_protected_file("package-lock.json")); + assert!(is_protected_file("node_modules/foo/package-lock.json")); + assert!(is_protected_file("image.PNG")); + assert!(is_protected_file("file.pdf")); + assert!(is_protected_file("lib.so")); + } + + #[test] + fn test_is_protected_file_allows_source() { + assert!(!is_protected_file("src/main.rs")); + assert!(!is_protected_file("src/App.tsx")); + assert!(!is_protected_file("package.json")); + } + + #[test] + fn test_is_text_allowed_extensions() { + assert!(is_text_allowed("src/main.rs")); + assert!(is_text_allowed("App.tsx")); + assert!(is_text_allowed("config.json")); + assert!(is_text_allowed("README.md")); + assert!(is_text_allowed(".env")); + assert!(is_text_allowed(".gitignore")); + } + + #[test] + fn test_is_text_allowed_no_extension() { + assert!(is_text_allowed("Dockerfile")); + assert!(is_text_allowed("Makefile")); + } + + #[test] + fn test_is_text_allowed_rejects_binary_ext() { + assert!(!is_text_allowed("photo.png")); + assert!(!is_text_allowed("doc.pdf")); + } +} diff --git a/src-tauri/src/commands/auto_check.rs b/src-tauri/src/commands/auto_check.rs new file mode 100644 index 0000000..c38b501 --- /dev/null +++ b/src-tauri/src/commands/auto_check.rs @@ -0,0 +1,57 @@ +use std::path::Path; +use std::process::Command; +use std::time::{Duration, Instant}; + +pub fn auto_check(root: &Path) -> Result<(), String> { + let start = Instant::now(); + let timeout = Duration::from_secs(120); + + let pkg = root.join("package.json"); + let cargo = root.join("Cargo.toml"); + let pyproject = root.join("pyproject.toml"); + let reqs = root.join("requirements.txt"); + + if pkg.exists() { + let mut cmd = Command::new("npm"); + cmd.arg("-s").arg("run").arg("build").current_dir(root); + let out = cmd.output(); + if start.elapsed() > timeout { + return Err("AUTO_CHECK_TIMEOUT".into()); + } + if let Ok(o) = out { + if !o.status.success() { + let mut cmd2 = Command::new("npm"); + cmd2.arg("-s").arg("test").current_dir(root); + let o2 = cmd2.output().map_err(|e| e.to_string())?; + if !o2.status.success() { + return Err("AUTO_CHECK_NODE_FAILED".into()); + } + } + } else { + return Err("AUTO_CHECK_NODE_FAILED".into()); + } + } + + if cargo.exists() { + let mut cmd = Command::new("cargo"); + cmd.arg("check").current_dir(root); + let o = cmd.output().map_err(|e| e.to_string())?; + if start.elapsed() > timeout { + return Err("AUTO_CHECK_TIMEOUT".into()); + } + if !o.status.success() { + return Err("AUTO_CHECK_RUST_FAILED".into()); + } + } + + if pyproject.exists() || reqs.exists() { + let mut cmd = Command::new("python3"); + cmd.arg("-c").arg("print('ok')").current_dir(root); + let o = cmd.output().map_err(|e| e.to_string())?; + if !o.status.success() { + return Err("AUTO_CHECK_PY_FAILED".into()); + } + } + + Ok(()) +} diff --git a/src-tauri/src/commands/folder_links.rs b/src-tauri/src/commands/folder_links.rs new file mode 100644 index 0000000..a2ed209 --- /dev/null +++ b/src-tauri/src/commands/folder_links.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FolderLinks { + pub paths: Vec, +} + +const FILENAME: &str = "folder_links.json"; + +pub fn load_folder_links(app_data_dir: &Path) -> FolderLinks { + let p = app_data_dir.join(FILENAME); + if let Ok(s) = fs::read_to_string(&p) { + if let Ok(links) = serde_json::from_str::(&s) { + return links; + } + } + FolderLinks::default() +} + +pub fn save_folder_links(app_data_dir: &Path, links: &FolderLinks) -> Result<(), String> { + fs::create_dir_all(app_data_dir).map_err(|e| e.to_string())?; + let p = app_data_dir.join(FILENAME); + fs::write( + &p, + serde_json::to_string_pretty(links).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/generate_actions.rs b/src-tauri/src/commands/generate_actions.rs new file mode 100644 index 0000000..2c14b8a --- /dev/null +++ b/src-tauri/src/commands/generate_actions.rs @@ -0,0 +1,153 @@ +//! v2.4: Build ActionPlan from analyze report (recommendations → actions). + +use std::path::Path; + +use crate::types::{ActionItem, ActionKind, ActionPlan, AnalyzeReport, GenerateActionsPayload}; + +fn rel(p: &str) -> String { + p.replace('\\', "/") +} + +fn mk_id(prefix: &str, n: usize) -> String { + format!("{}-{}", prefix, n) +} + +fn report_mentions_readme(report: &AnalyzeReport) -> bool { + report + .findings + .iter() + .any(|f| f.title.contains("README") || f.details.to_lowercase().contains("readme")) + || report + .recommendations + .iter() + .any(|r| r.title.to_lowercase().contains("readme") || r.details.to_lowercase().contains("readme")) +} + +fn report_mentions_gitignore(report: &AnalyzeReport) -> bool { + report + .findings + .iter() + .any(|f| f.title.contains("gitignore") || f.details.to_lowercase().contains("gitignore")) + || report + .recommendations + .iter() + .any(|r| r.title.to_lowercase().contains("gitignore") || r.details.to_lowercase().contains("gitignore")) +} + +fn report_mentions_tests(report: &AnalyzeReport) -> bool { + report + .findings + .iter() + .any(|f| f.title.contains("tests") || f.details.to_lowercase().contains("тест")) + || report + .recommendations + .iter() + .any(|r| r.title.to_lowercase().contains("test") || r.details.to_lowercase().contains("тест")) +} + +pub fn build_actions_from_report(report: &AnalyzeReport, mode: &str) -> Vec { + let mut out: Vec = vec![]; + + if report_mentions_readme(report) { + out.push(ActionItem { + id: mk_id("action", out.len() + 1), + kind: ActionKind::CreateFile, + path: rel("README.md"), + content: Some( + "# PAPA YU Project\n\n## Описание\n\nКратко опишите проект.\n\n## Запуск\n\n- dev: ...\n- build: ...\n\n## Структура\n\n- src/\n- tests/\n".into(), + ), + summary: "Добавить README.md".into(), + rationale: "Улучшает понимание проекта и снижает риск ошибок при работе с кодом.".into(), + tags: vec!["docs".into(), "quality".into()], + risk: "low".into(), + }); + } + + if report_mentions_gitignore(report) { + out.push(ActionItem { + id: mk_id("action", out.len() + 1), + kind: ActionKind::CreateFile, + path: rel(".gitignore"), + content: Some( + "node_modules/\ndist/\nbuild/\n.next/\ncoverage/\n.env\n.env.*\n.DS_Store\n".into(), + ), + summary: "Добавить .gitignore".into(), + rationale: "Исключает мусор и потенциально секретные файлы из репозитория.".into(), + tags: vec!["quality".into(), "security".into()], + risk: "low".into(), + }); + } + + if report_mentions_tests(report) { + out.push(ActionItem { + id: mk_id("action", out.len() + 1), + kind: ActionKind::CreateDir, + path: rel("tests"), + content: None, + summary: "Создать папку tests/".into(), + rationale: "Готовит структуру под тесты.".into(), + tags: vec!["quality".into(), "tests".into()], + risk: "low".into(), + }); + out.push(ActionItem { + id: mk_id("action", out.len() + 1), + kind: ActionKind::CreateFile, + path: rel("tests/smoke.test.txt"), + content: Some("TODO: add smoke tests\n".into()), + summary: "Добавить tests/smoke.test.txt".into(), + rationale: "Минимальный маркер тестов. Замените на реальные тесты позже.".into(), + tags: vec!["tests".into()], + risk: "low".into(), + }); + } + + if mode == "balanced" { + let root = Path::new(&report.path); + let has_node = root.join("package.json").exists(); + let has_react = root.join("package.json").exists() && (root.join("src").join("App.jsx").exists() || root.join("src").join("App.tsx").exists()); + if has_node || has_react { + out.push(ActionItem { + id: mk_id("action", out.len() + 1), + kind: ActionKind::CreateFile, + path: rel(".prettierrc"), + content: Some("{\n \"singleQuote\": true,\n \"semi\": true\n}\n".into()), + summary: "Добавить .prettierrc".into(), + rationale: "Стабилизирует форматирование кода.".into(), + tags: vec!["quality".into()], + risk: "low".into(), + }); + } + } + + out +} + +#[tauri::command] +pub async fn generate_actions(payload: GenerateActionsPayload) -> Result { + let path = payload.path.clone(); + let mode = if payload.mode.is_empty() { "safe" } else { payload.mode.as_str() }; + + let report = crate::commands::analyze_project(vec![path.clone()], None)?; + let mut actions = build_actions_from_report(&report, mode); + + if !payload.selected.is_empty() { + let sel: Vec = payload.selected.iter().map(|s| s.to_lowercase()).collect(); + actions = actions + .into_iter() + .filter(|a| { + let txt = format!("{} {} {} {:?}", a.summary, a.rationale, a.risk, a.tags).to_lowercase(); + sel.iter().any(|k| txt.contains(k)) + }) + .collect(); + } + + let warnings = vec!["Все изменения применяются только через предпросмотр и могут быть отменены.".into()]; + + Ok(ActionPlan { + plan_id: format!("plan-{}", chrono::Utc::now().timestamp_millis()), + root_path: path, + title: "План исправлений (MVP)".into(), + actions, + warnings, + }) +} diff --git a/src-tauri/src/commands/generate_actions_from_report.rs b/src-tauri/src/commands/generate_actions_from_report.rs new file mode 100644 index 0000000..979270d --- /dev/null +++ b/src-tauri/src/commands/generate_actions_from_report.rs @@ -0,0 +1,164 @@ +//! v3.2: generate Action[] from AnalyzeReport (safe create-only, no LLM). + +use std::path::Path; + +use crate::tx::safe_join; +use crate::types::{Action, ActionKind, AnalyzeReport, GenerateActionsResult}; + +const MAX_ACTIONS: usize = 20; + +/// Forbidden path segments (no write under these). +const FORBIDDEN: &[&str] = &[".git", "node_modules", "target", "dist", "build", ".next"]; + +fn rel(p: &str) -> String { + p.replace('\\', "/") +} + +fn is_path_forbidden(rel: &str) -> bool { + let r = rel.trim_start_matches('/'); + if r.contains("..") || rel.starts_with('/') || rel.starts_with('\\') { + return true; + } + let parts: Vec<&str> = r.split('/').collect(); + for part in &parts { + if FORBIDDEN.contains(part) { + return true; + } + } + false +} + +fn has_readme(root: &Path) -> bool { + ["README.md", "README.MD", "README.txt", "README"] + .iter() + .any(|f| root.join(f).exists()) +} + +fn has_gitignore(root: &Path) -> bool { + root.join(".gitignore").exists() +} + +fn has_license(root: &Path) -> bool { + ["LICENSE", "LICENSE.md", "LICENSE.txt"] + .iter() + .any(|f| root.join(f).exists()) +} + +fn has_src(root: &Path) -> bool { + root.join("src").is_dir() +} + +fn has_tests(root: &Path) -> bool { + root.join("tests").is_dir() +} + +#[tauri::command] +pub async fn generate_actions_from_report( + path: String, + report: AnalyzeReport, + mode: String, +) -> GenerateActionsResult { + let _ = report; // reserved for future use (e.g. narrative/signals) + let root = Path::new(&path); + if !root.exists() || !root.is_dir() { + return GenerateActionsResult { + ok: false, + actions: vec![], + skipped: vec![], + error: Some("path not found".into()), + error_code: Some("PATH_NOT_FOUND".into()), + }; + } + + let create_only = mode == "safe_create_only" || mode == "safe" || mode.is_empty(); + let mut actions: Vec = vec![]; + let mut skipped: Vec = vec![]; + + // 1. README + if !has_readme(root) { + let rel_path = rel("README.md"); + if is_path_forbidden(&rel_path) { + skipped.push("README.md (forbidden path)".into()); + } else if safe_join(root, &rel_path).is_ok() { + actions.push(Action { + kind: ActionKind::CreateFile, + path: rel_path.clone(), + content: Some( + "# Project\n\n## Описание\n\nКратко опишите проект.\n\n## Запуск\n\n- dev: ...\n- build: ...\n\n## Структура\n\n- src/\n- tests/\n".into(), + ), + }); + } + } + + // 2. .gitignore + if !has_gitignore(root) { + let rel_path = rel(".gitignore"); + if is_path_forbidden(&rel_path) { + skipped.push(".gitignore (forbidden path)".into()); + } else if safe_join(root, &rel_path).is_ok() { + actions.push(Action { + kind: ActionKind::CreateFile, + path: rel_path, + content: Some( + "node_modules/\ndist/\nbuild/\n.next/\ncoverage/\n.env\n.env.*\n.DS_Store\n.target/\n".into(), + ), + }); + } + } + + // 3. LICENSE + if !has_license(root) { + let rel_path = rel("LICENSE"); + if is_path_forbidden(&rel_path) { + skipped.push("LICENSE (forbidden path)".into()); + } else if safe_join(root, &rel_path).is_ok() { + actions.push(Action { + kind: ActionKind::CreateFile, + path: rel_path, + content: Some("MIT License\n\nCopyright (c) \n".into()), + }); + } + } + + // 4. tests/ + tests/.gitkeep (when src exists and tests missing) + if has_src(root) && !has_tests(root) { + let dir_path = rel("tests"); + if !is_path_forbidden(&dir_path) && safe_join(root, &dir_path).is_ok() { + actions.push(Action { + kind: ActionKind::CreateDir, + path: dir_path, + content: None, + }); + } + let keep_path = rel("tests/.gitkeep"); + if !is_path_forbidden(&keep_path) && safe_join(root, &keep_path).is_ok() { + actions.push(Action { + kind: ActionKind::CreateFile, + path: keep_path, + content: Some("".into()), + }); + } + } + + if create_only { + // v3.3: only CreateFile and CreateDir; any other kind would be skipped (we already only create) + } + + if actions.len() > MAX_ACTIONS { + return GenerateActionsResult { + ok: false, + actions: vec![], + skipped: vec![format!("more than {} actions", MAX_ACTIONS)], + error: Some(format!("max {} actions per run", MAX_ACTIONS)), + error_code: Some("TOO_MANY_ACTIONS".into()), + }; + } + + GenerateActionsResult { + ok: true, + actions, + skipped, + error: None, + error_code: None, + } +} diff --git a/src-tauri/src/commands/get_project_profile.rs b/src-tauri/src/commands/get_project_profile.rs new file mode 100644 index 0000000..add8466 --- /dev/null +++ b/src-tauri/src/commands/get_project_profile.rs @@ -0,0 +1,190 @@ +//! v2.4.3: detect project profile by path (type, limits, goal_template). + +use std::path::Path; +use std::time::Instant; + +use tauri::{Emitter, Window}; + +use crate::types::{ProjectLimits, ProjectProfile, ProjectType}; + +fn has_file(root: &Path, rel: &str) -> bool { + root.join(rel).is_file() +} +fn has_dir(root: &Path, rel: &str) -> bool { + root.join(rel).is_dir() +} + +pub fn detect_project_type(root: &Path) -> ProjectType { + if has_file(root, "next.config.js") + || has_file(root, "next.config.mjs") + || has_file(root, "next.config.ts") + || (has_dir(root, "app") || has_dir(root, "pages")) + { + return ProjectType::NextJs; + } + + if has_file(root, "vite.config.ts") + || has_file(root, "vite.config.js") + || has_file(root, "vite.config.mjs") + { + return ProjectType::ReactVite; + } + + if has_file(root, "package.json") { + return ProjectType::Node; + } + + if has_file(root, "Cargo.toml") { + return ProjectType::Rust; + } + + if has_file(root, "pyproject.toml") + || has_file(root, "requirements.txt") + || has_file(root, "setup.py") + { + return ProjectType::Python; + } + + ProjectType::Unknown +} + +fn build_goal_template(pt: &ProjectType) -> String { + let tone = "Отвечай коротко и по-человечески, как коллега в чате."; + match pt { + ProjectType::ReactVite => format!("Цель: {{goal}}\nКонтекст: это React+Vite проект. Действуй безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + ProjectType::NextJs => format!("Цель: {{goal}}\nКонтекст: это Next.js проект. Действуй безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + ProjectType::Rust => format!("Цель: {{goal}}\nКонтекст: это Rust/Cargo проект. Действуй безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + ProjectType::Python => format!("Цель: {{goal}}\nКонтекст: это Python проект. Действуй безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + ProjectType::Node => format!("Цель: {{goal}}\nКонтекст: это Node проект. Действуй безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + ProjectType::Unknown => format!("Цель: {{goal}}\nКонтекст: тип проекта не определён. Действуй максимально безопасно: только preview → apply_tx → auto_check → undo при ошибке.\nОграничения: никаких shell; только safe FS; не трогать секреты.\n{}", tone), + } +} + +/// v2.4.4: get limits for a path (used by apply_actions_tx and run_batch for max_actions_per_tx and timeout). +pub fn get_project_limits(root: &Path) -> ProjectLimits { + default_limits(&detect_project_type(root)) +} + +fn default_limits(pt: &ProjectType) -> ProjectLimits { + match pt { + ProjectType::ReactVite | ProjectType::NextJs | ProjectType::Node => ProjectLimits { + max_files: 50_000, + timeout_sec: 60, + max_actions_per_tx: 25, + }, + ProjectType::Rust => ProjectLimits { + max_files: 50_000, + timeout_sec: 60, + max_actions_per_tx: 20, + }, + ProjectType::Python => ProjectLimits { + max_files: 50_000, + timeout_sec: 60, + max_actions_per_tx: 20, + }, + ProjectType::Unknown => ProjectLimits { + max_files: 30_000, + timeout_sec: 45, + max_actions_per_tx: 15, + }, + } +} + +#[tauri::command] +pub async fn get_project_profile(window: Window, path: String) -> Result { + let root = Path::new(&path); + if !root.exists() { + return Err("PATH_NOT_FOUND".to_string()); + } + if !root.is_dir() { + return Err("PATH_NOT_DIRECTORY".to_string()); + } + + let _ = window.emit("analyze_progress", "Определяю профиль проекта…"); + + let start = Instant::now(); + let project_type = detect_project_type(root); + let limits = default_limits(&project_type); + + let safe_mode = true; + + let max_attempts = match project_type { + ProjectType::Unknown => 2, + _ => 3, + }; + + let goal_template = build_goal_template(&project_type); + + let _elapsed = start.elapsed(); + + Ok(ProjectProfile { + path, + project_type, + safe_mode, + max_attempts, + goal_template, + limits, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_detect_project_type_unknown_empty() { + let dir = tempfile::TempDir::new().unwrap(); + let root = dir.path(); + assert_eq!(detect_project_type(root), ProjectType::Unknown); + } + + #[test] + fn test_detect_project_type_node() { + let dir = tempfile::TempDir::new().unwrap(); + let root = dir.path(); + fs::write(root.join("package.json"), "{}").unwrap(); + assert_eq!(detect_project_type(root), ProjectType::Node); + } + + #[test] + fn test_detect_project_type_rust() { + let dir = tempfile::TempDir::new().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[package]\nname = \"x\"").unwrap(); + assert_eq!(detect_project_type(root), ProjectType::Rust); + } + + #[test] + fn test_detect_project_type_react_vite() { + let dir = tempfile::TempDir::new().unwrap(); + let root = dir.path(); + fs::write(root.join("vite.config.ts"), "export default {}").unwrap(); + assert_eq!(detect_project_type(root), ProjectType::ReactVite); + } + + #[test] + fn test_detect_project_type_python() { + let dir = tempfile::TempDir::new().unwrap(); + let root = dir.path(); + fs::write(root.join("pyproject.toml"), "[project]\nname = \"x\"").unwrap(); + assert_eq!(detect_project_type(root), ProjectType::Python); + } + + #[test] + fn test_get_project_limits_unknown() { + let dir = tempfile::TempDir::new().unwrap(); + let limits = get_project_limits(dir.path()); + assert_eq!(limits.max_actions_per_tx, 15); + assert_eq!(limits.timeout_sec, 45); + } + + #[test] + fn test_get_project_limits_rust() { + let dir = tempfile::TempDir::new().unwrap(); + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"").unwrap(); + let limits = get_project_limits(dir.path()); + assert_eq!(limits.max_actions_per_tx, 20); + assert_eq!(limits.timeout_sec, 60); + } +} diff --git a/src-tauri/src/commands/llm_planner.rs b/src-tauri/src/commands/llm_planner.rs new file mode 100644 index 0000000..a0f8018 --- /dev/null +++ b/src-tauri/src/commands/llm_planner.rs @@ -0,0 +1,1352 @@ +//! LLM-планировщик: генерация плана действий через OpenAI-совместимый API. +//! +//! Конфигурация через переменные окружения: +//! - `PAPAYU_LLM_API_URL` — URL API (например https://api.openai.com/v1/chat/completions или http://localhost:11434/v1/chat/completions для Ollama) +//! - `PAPAYU_LLM_API_KEY` — API-ключ (опционально для локальных API вроде Ollama) +//! - `PAPAYU_LLM_MODEL` — модель (по умолчанию gpt-4o-mini для OpenAI, для Ollama — например llama3.2) +//! - `PAPAYU_LLM_MODE` — режим: `chat` (инженер-коллега) или `fixit` (обязан вернуть патч + проверку); по умолчанию `chat` +//! - `PAPAYU_LLM_STRICT_JSON` — если `1`/`true`: добавляет `response_format: { type: "json_schema", ... }` (OpenAI Structured Outputs; Ollama может не поддерживать) +//! - `PAPAYU_LLM_TEMPERATURE` — температура генерации (по умолчанию 0 для детерминизма) +//! - `PAPAYU_LLM_MAX_TOKENS` — макс. токенов ответа (по умолчанию 65536) +//! - `PAPAYU_TRACE` — если `1`/`true`: пишет трассу в `.papa-yu/traces/.json` + +use crate::context; +use crate::memory; +use crate::types::{Action, ActionKind, AgentPlan}; +use jsonschema::JSONSchema; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::time::Duration; +use uuid::Uuid; + +const SCHEMA_RAW: &str = include_str!("../../config/llm_response_schema.json"); + +pub(crate) fn schema_hash() -> String { + let mut hasher = Sha256::new(); + hasher.update(SCHEMA_RAW.as_bytes()); + format!("{:x}", hasher.finalize()) +} + +#[derive(serde::Serialize)] +struct ChatMessage { + role: String, + content: String, +} + +#[derive(Clone, serde::Serialize)] +struct ResponseFormatJsonSchema { + #[serde(rename = "type")] + ty: String, + json_schema: ResponseFormatJsonSchemaInner, +} + +#[derive(Clone, serde::Serialize)] +struct ResponseFormatJsonSchemaInner { + name: String, + schema: serde_json::Value, + strict: bool, +} + +#[derive(serde::Serialize)] +struct ChatRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + presence_penalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + frequency_penalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + response_format: Option, +} + +#[derive(Deserialize)] +struct ChatChoice { + message: ChatMessageResponse, +} + +#[derive(Deserialize)] +struct ChatMessageResponse { + content: Option, +} + +#[derive(Deserialize)] +struct ChatResponse { + choices: Option>, +} + +/// Пишет лог-ивент в stderr (формат: [trace_id] EVENT key=value ...). +fn log_llm_event(trace_id: &str, event: &str, pairs: &[(&str, String)]) { + let line = pairs + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() + .join(" "); + eprintln!("[{}] {} {}", trace_id, event, line); +} + +const INPUT_CHARS_FOR_CAP: usize = 80_000; +const MAX_TOKENS_WHEN_LARGE_INPUT: u32 = 4096; + +/// Маскирует секреты в строке (raw_content) при PAPAYU_TRACE_RAW=1. +fn redact_secrets(s: &str) -> String { + let mut out = s.to_string(); + let mut pos = 0; + // sk-... (OpenAI keys) — маскируем все вхождения + while let Some(start) = out[pos..].find("sk-") { + let abs_start = pos + start; + let after = &out[abs_start + 3..]; + let rest_len = after.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '-').count(); + let end = abs_start + 3 + rest_len.min(50); + if end <= out.len() { + out.replace_range(abs_start..end, "__REDACTED_API_KEY__"); + pos = abs_start + 18; // len("__REDACTED_API_KEY__") + } else { + break; + } + } + // Bearer token + pos = 0; + while let Some(start) = out[pos..].find("Bearer ") { + let abs_start = pos + start; + let after = &out[abs_start + 7..]; + let rest_len = after.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '.').count(); + let end = abs_start + 7 + rest_len.min(60); + if end <= out.len() { + out.replace_range(abs_start..end, "__REDACTED_BEARER__"); + pos = abs_start + 17; // len("__REDACTED_BEARER__") + } else { + break; + } + } + out +} + +/// Сохраняет трассу в .papa-yu/traces/.json при PAPAYU_TRACE=1. +/// По умолчанию raw_content не сохраняется (риск секретов); PAPAYU_TRACE_RAW=1 — сохранять (с маскировкой). +fn write_trace(project_path: &str, trace_id: &str, trace: &mut serde_json::Value) { + // Добавляем config_snapshot для воспроизводимости + let config_snapshot = serde_json::json!({ + "schema_version": LLM_PLAN_SCHEMA_VERSION, + "schema_hash": schema_hash(), + "strict_json": std::env::var("PAPAYU_LLM_STRICT_JSON").unwrap_or_default(), + "trace_raw": std::env::var("PAPAYU_TRACE_RAW").unwrap_or_default(), + "normalize_eol": std::env::var("PAPAYU_NORMALIZE_EOL").unwrap_or_default(), + "memory_autopatch": std::env::var("PAPAYU_MEMORY_AUTOPATCH").unwrap_or_default(), + "max_tokens": std::env::var("PAPAYU_LLM_MAX_TOKENS").unwrap_or_default(), + "temperature": std::env::var("PAPAYU_LLM_TEMPERATURE").unwrap_or_default(), + "timeout_sec": std::env::var("PAPAYU_LLM_TIMEOUT_SEC").unwrap_or_default(), + }); + if std::env::var("PAPAYU_TRACE") + .map(|s| matches!(s.trim().to_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false) + { + let trace_raw = std::env::var("PAPAYU_TRACE_RAW") + .map(|s| matches!(s.trim().to_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + + if !trace_raw { + if let Some(obj) = trace.as_object_mut() { + if let Some(raw) = obj.remove("raw_content") { + if let Some(s) = raw.as_str() { + obj.insert("raw_content_redacted".into(), serde_json::Value::Bool(true)); + let preview: String = s.chars().take(200).collect(); + obj.insert("raw_content_preview".into(), serde_json::Value::String(format!("{}... ({} chars)", preview, s.len()))); + } + } + } + } else if let Some(obj) = trace.as_object_mut() { + if let Some(raw) = obj.get("raw_content").and_then(|v| v.as_str()) { + let redacted = redact_secrets(raw); + obj.insert("raw_content".into(), serde_json::Value::String(redacted)); + } + } + + if let Some(obj) = trace.as_object_mut() { + obj.insert("config_snapshot".into(), config_snapshot); + } + + if let Ok(root) = Path::new(project_path).canonicalize() { + let trace_dir = root.join(".papa-yu").join("traces"); + let _ = fs::create_dir_all(&trace_dir); + let trace_file = trace_dir.join(format!("{}.json", trace_id)); + let _ = fs::write(&trace_file, serde_json::to_string_pretty(trace).unwrap_or_default()); + } + } +} + +/// System prompt: режим Chat (инженер-коллега). +pub const CHAT_SYSTEM_PROMPT: &str = r#"Ты — мой инженерный ассистент внутри программы для создания, анализа и исправления кода. +Оператор один: я. Общайся как с коллегой-человеком: естественно, кратко, без канцелярщины и без самопрезентаций. + +Главная цель: давать точные, проверяемые ответы по программированию и работе с проектом. +Стиль: "что вижу → что предлагаю → что сделать". + +Ключевые правила: +- Не выдумывай факты о проекте. Если ты не читал файл/лог/результат — так и скажи. +- Никогда не утверждай, что ты что-то запускал/проверял, если не вызывал инструмент и не видел вывод. +- Если данных не хватает — задай максимум 2 уточняющих вопроса. Если можно двигаться без уточнений — двигайся. +- Если предлагаешь изменения — показывай конкретный patch/diff и объясняй 2–5 короткими пунктами "почему так". +- Всегда предлагай шаг проверки (тест/команда/репро). +- Если есть риск (удаление данных, миграции, security) — предупреждай и предлагай безопасный вариант. + +Инструменты: +- Используй инструменты для чтения файлов, поиска, логов, тестов и применения патчей, когда это повышает точность. +- Сначала собирай факты (read/search/logs), потом делай выводы, потом патч."#; + +/// System prompt: режим Fix-it (обязан вернуть патч + проверку). +pub const FIXIT_SYSTEM_PROMPT: &str = r#"Ты — режим Fix-it внутри моей IDE-программы. +Твоя задача: минимальным и безопасным изменением исправить проблему и дать проверяемые шаги. + +Выход должен содержать: +1) Краткий диагноз (1–3 пункта) +2) Patch/diff (обязательно) +3) Команды проверки (обязательно) +4) Риски/побочки (если есть) + +Правила: +- Не выдумывай содержимое файлов/логов — сначала прочитай их через инструменты. +- Не делай широкие рефакторы без запроса: исправляй минимально. +- Если не хватает данных, можно задать 1 вопрос; иначе действуй."#; + +/// Формальная версия схемы ответа (для воспроизводимости и будущего v2). +pub const LLM_PLAN_SCHEMA_VERSION: u32 = 1; + +/// System prompt: режим Fix-plan (один JSON, context_requests, план → подтверждение → применение). +/// Режим через user.output_format: "plan" = только план, "apply" = действия. +pub const FIX_PLAN_SYSTEM_PROMPT: &str = r#"Ты — инженерный ассистент внутри программы для создания, анализа и исправления кода. Оператор один: я. +Всегда отвечай ОДНИМ валидным JSON-объектом. Никакого текста вне JSON. + +Режимы (смотри user.output_format в ENGINEERING_MEMORY): +- user.output_format == "plan" (Fix-plan): НЕ предлагай применять изменения. Верни actions пустым массивом []. + Опиши диагноз и пошаговый план в summary. Если нужно больше данных — заполни context_requests. +- user.output_format == "apply" (Apply): Верни actions (или proposed_changes.actions) с конкретными изменениями файлов/директорий. + summary: что изменено и как проверить (используй project.default_test_command если задан). + Если изменений не требуется — верни actions: [] и summary, начинающийся с "NO_CHANGES:" (строго). + +Если output_format не задан или "patch_first"/"plan_first" — верни actions как обычно (массив или объект с actions). + +Правила: +- Не выдумывай содержимое файлов/логов. Если нужно — запроси через context_requests. +- Никогда не утверждай, что тесты/команды запускались, если их не запускало приложение. +- Если данных не хватает — задай максимум 2 вопроса в questions и/или добавь context_requests. +- Минимальные изменения. Без широких рефакторингов без явного запроса. + +Схема JSON (всегда либо массив actions, либо объект): +- actions: массив { kind, path, content } — kind: CREATE_FILE|CREATE_DIR|UPDATE_FILE|DELETE_FILE|DELETE_DIR +- proposed_changes.actions: альтернативное место для actions +- summary: string (диагноз + план для plan, что сделано для apply) +- context_requests: [{ type: "read_file"|"search"|"logs"|"env", path?, start_line?, end_line?, query?, glob?, source?, last_n? }] +- memory_patch: object (только ключи из whitelist: user.*, project.*)"#; + +/// Возвращает system prompt по режиму (PAPAYU_LLM_MODE: chat | fixit | fix-plan). +fn get_system_prompt_for_mode() -> &'static str { + let mode = std::env::var("PAPAYU_LLM_MODE").unwrap_or_else(|_| "chat".into()); + match mode.trim().to_lowercase().as_str() { + "fixit" | "fix-it" | "fix_it" => FIXIT_SYSTEM_PROMPT, + "fix-plan" | "fix_plan" => FIX_PLAN_SYSTEM_PROMPT, + _ => CHAT_SYSTEM_PROMPT, + } +} + +/// Проверяет, включён ли LLM-планировщик (задан URL). +pub fn is_llm_configured() -> bool { + std::env::var("PAPAYU_LLM_API_URL") + .map(|s| !s.trim().is_empty()) + .unwrap_or(false) +} + +/// Строит промпт для LLM: путь, полное содержимое проекта (все файлы), отчёт, цель, стиль дизайна и опционально контекст трендов. +/// ИИ настроен так, чтобы самостоятельно использовать дизайн и тренды при предложениях. +fn build_prompt( + path: &str, + report_json: &str, + user_goal: &str, + project_content: Option<&str>, + design_style: Option<&str>, + trends_context: Option<&str>, +) -> String { + let content_block = project_content + .filter(|s| !s.trim().is_empty()) + .map(|s| format!("\n\nПолное содержимое файлов проекта (анализируй всё, не только три файла):\n{}\n", s)) + .unwrap_or_else(|| "\n\nПроект пуст или папка не найдена. Можешь создавать программу с нуля: полную структуру (package.json, src/, конфиги, исходники, README, .gitignore и т.д.).\n".to_string()); + + let content_empty = project_content + .map(|s| { + s.trim().is_empty() + || s.contains("пуста") + || s.contains("не найдена") + || s.contains("нет релевантных") + }) + .unwrap_or(true); + let create_from_scratch = content_empty + || user_goal.to_lowercase().contains("с нуля") + || user_goal.to_lowercase().contains("from scratch") + || user_goal.to_lowercase().contains("создать проект"); + + let extra = if create_from_scratch { + "\nВажно: пользователь может просить создать проект с нуля. Предлагай полный набор файлов и папок (package.json, src/index.ts, README.md, .gitignore, конфиги и т.д.) в виде массива действий CREATE_DIR и CREATE_FILE." + } else { + "" + }; + + let design_block = design_style + .filter(|s| !s.trim().is_empty()) + .map(|s| { + let lower = s.to_lowercase(); + let hint = if lower.contains("material") || lower.contains("материал") { + "Применяй Material Design: компоненты и гайдлайны из material.io, структура и стили в духе Material UI (MUI)." + } else if lower.contains("tailwind") || lower.contains("shadcn") || lower.contains("shadcn/ui") { + "Применяй Tailwind CSS и/или shadcn/ui: утилитарные классы, компоненты из shadcn (радиусы, тени, типографика из сторонних ресурсов shadcn/ui)." + } else if lower.contains("bootstrap") { + "Применяй Bootstrap: сетка, компоненты, утилиты из Bootstrap (getbootstrap.com)." + } else if lower.contains("сторонн") || lower.contains("third-party") || lower.contains("внешн") { + "Используй дизайн из сторонних ресурсов: популярные UI-библиотеки, дизайн-системы (Material, Ant Design, Chakra, Radix и т.д.), подключай через npm/CDN и применяй в разметке и стилях." + } else { + "Применяй свой дизайн ИИ: современный, читаемый UI, консистентные отступы, типографика и цвета; при создании с нуля добавляй CSS/конфиг под выбранный стиль." + }; + format!("\n\nСтиль дизайна: {}. {}", s.trim(), hint) + }) + .unwrap_or_else(|| "\n\nДизайн: самостоятельно применяй современный консистентный дизайн при создании или изменении UI — свой (ИИ) или из известных систем (Material, Tailwind/shadcn, Bootstrap). Делай это по умолчанию, без явного выбора пользователем.".to_string()); + + let trends_block = trends_context + .filter(|s| !s.trim().is_empty()) + .map(|s| format!("\n\nИспользуй самостоятельно актуальные рекомендации и тренды (учитывай при предложениях и улучшениях):\n{}\n", s)) + .unwrap_or_else(String::new); + + format!( + r#"Контекст задачи и проекта (используй только эти данные; роль и правила заданы в system message). + +ПОЛНОМОЧИЯ: read/write файлов (через план действий), формирование плана под git/PR. «Готово» = план содержит код/патчи + шаги проверки (lint, test). Реальное выполнение делает приложение по твоему плану. + +ЕДИНЫЙ КОНТУР РАЗРАБОТКИ (обязательный порядок по возможности): +1. Прочитай ТЗ → сформируй PLAN.md (файлы, модули, шаги, критерии готовности). +2. Создай каркас проекта (scaffold). +3. Реализуй MVP. +4. Добавь тесты. +5. Обеспечь запуск: make lint, make test, make build (или эквивалент: npm run lint/test/build, pytest, mvn test и т.д.). +6. Если что-то падает — план должен предусматривать исправления до успеха; итог — CHANGELOG или отчёт. + +РЕПОЗИТОРИЙ-ШАБЛОН: единая структура команд (агент всегда знает, что делать): +- make setup (или npm install / pip install / mvn install) +- make lint — ruff/eslint/spotless по стеку +- make test — pytest/vitest/junit по стеку +- make run — запуск приложения +- make build — сборка артефакта +При создании проекта добавляй Makefile (или явно npm/pnpm/mvn/gradle скрипты) с этими целями. + +ПО СТЕКУ (минимум): +- Python: ruff check, pytest, black/ruff format; CLI Typer/Click, сайт FastAPI+uvicorn, десктоп PySide6. +- JS/TS: npm run lint (eslint), npm test (vitest/jest), npm run build, npm run dev; CLI commander/oclif, сайт React/Vue+Vite, десктоп Electron/Tauri. +- Java: mvn test / gradle test, spotless/checkstyle, mvn package; CLI Picocli, сайт Spring Boot, десктоп JavaFX. + +БОЕВОЙ ШАБЛОН PYTHON + FASTAPI (САЙТ) — используй при создании сайта на FastAPI: +Структура: app/ (__init__.py, main.py), app/api/ (__init__.py, health.py), app/core/ (__init__.py, config.py), tests/ (__init__.py, test_health.py), .github/workflows/ci.yml, .gitignore, Makefile, README.md, pyproject.toml, ruff.toml. +Команды: make setup (pip install -e ".[dev]"), make lint (ruff check . + ruff format --check .), make test (pytest), make run (uvicorn app.main:app --reload --host 0.0.0.0 --port 8000), make build (python -c "import app.main; print('ok')"). +Файлы: pyproject.toml — project name/version, requires-python >=3.11, dependencies fastapi>=0.110, uvicorn[standard]>=0.27, pydantic>=2.6; dev: pytest>=8.0, httpx>=0.27, ruff>=0.6; [tool.pytest.ini_options] testpaths=["tests"] addopts="-q". ruff.toml — line-length=100 target-version="py311" [lint] select=["E","F","I","B","UP"]. Makefile — цели setup, lint, test, run, build как выше. app/main.py — FastAPI(title, version), include_router(health_router). app/api/health.py — APIRouter(tags=["health"]), GET /health возвращает {{"status":"ok"}}. app/core/config.py — pydantic BaseModel Settings (app_name). tests/test_health.py — TestClient(app), GET /health, assert status_code 200, json == {{"status":"ok"}}. .github/workflows/ci.yml — checkout, setup-python 3.11, make setup, make lint, make test, make build. README.md — make setup, make run, ссылка на /health, make lint, make test. +Контракт для FastAPI: (1) Всегда начинай с PLAN.md (что меняешь, файлы, критерии готовности). (2) Любая фича = код + тест. (3) После каждого шага — make lint, make test; если упало — фикси до зелёного. (4) Итог: PR/патч + отчёт (что сделано, как запустить, как проверить). Интернет для FastAPI: только официальные доки FastAPI/Uvicorn/Pydantic, PyPI, официальные примеры; правило «нашёл решение → подтверди make test». + +ШАБЛОН PLAN.md (в корень при планировании): заголовок «План работ»; секции: Контекст (репо, цель, ограничения), Требования DoD (чеклист: функциональность, тесты make test, линт make lint, README, CI зелёный), Архитектура/Дизайн (модули app/main, app/api, app/core, app/db при БД; решения: БД SQLite/SQLAlchemy 2.x, миграции Alembic, тесты pytest + отдельная тестовая БД), План изменений по шагам (1 scaffold БД/миграции 2 сущность 3 CRUD endpoints 4 тесты 5 README), Риски/Вопросы. + +ПРИМЕР ФИЧИ CRUD (FastAPI + SQLite + Alembic + тесты): зависимости — sqlalchemy>=2.0, dev: alembic>=1.13. Слой БД: app/db/session.py (create_engine sqlite, SessionLocal), app/db/base.py (DeclarativeBase), app/db/deps.py (get_db yield Session), app/db/models.py (Item: id, name, description). Схемы: app/api/schemas.py (ItemCreate, ItemUpdate, ItemOut Pydantic). Роутер: app/api/items.py (prefix /items, POST/GET/GET list/PATCH/DELETE, Depends(get_db), 404 если не найден). main.py: include_router(items_router), Base.metadata.create_all(bind=engine) для dev. Тесты: tests/conftest.py (tempfile sqlite, override get_db, TestClient), tests/test_items.py (test_items_crud: create 201, get 200, list, patch, delete 204, get 404). Makefile: migrate = alembic upgrade head. README: секция DB migrations (alembic upgrade head). Alembic: alembic init, env.py — импорт Base и models, target_metadata=Base.metadata; alembic revision --autogenerate -m "create items", alembic upgrade head. + +ПРОТОКОЛ «ДОБАВИТЬ ФИЧУ» (для агента): (1) Создай/обнови PLAN.md (DoD + список файлов). (2) Реализуй минимальный endpoint + тест. (3) Запусти make test → исправь до зелёного. (4) make lint. (5) Обнови README. (6) Итог: один PR, CI зелёный. + +ИНТЕРНЕТ: используй только для официальной документации (docs.*, GitHub, PyPI, npm, Maven Central), проверки версий и примеров API. Любую найденную команду/конфиг — проверять запуском тестов/сборки. + +КОНТРАКТ (жёсткие правила): +1. Всегда начинай с PLAN.md (архитектура, файлы, команды, критерии готовности). +2. Всегда добавляй/обновляй README.md (setup, run, test). +3. Любая фича = код + тест. +4. После изменений всегда предусматривай запуск make lint и make test (и make build если есть). +5. Если неясно — делай разумное допущение, фиксируй в PLAN.md и README.md. +6. Не добавляй зависимости без явной причины. +7. Итог: план действий ведёт к PR/патчу + краткому отчёту «что сделано / как проверить». + +КРИТИЧЕСКИ ВАЖНО: При вводе пользователя выполняй команду в ПЕРВУЮ ОЧЕРЕДЬ (например: «помоги создать программу», «добавь README», «создай проект с нуля»). Формируй план действий (массив действий). НЕ предлагай сначала анализ — сразу план по запросу. +Форматы: (1) scaffold — структура, зависимости, базовые модули. (2) автокодер по ТЗ — фичи, тесты, документация. (3) репо/патчи — тесты, линтер, PR. (4) скрипты/автоматизации. Выбирай по формулировке пользователя. +Верни ТОЛЬКО валидный JSON: либо массив действий, либо объект {{ "actions": [...], "memory_patch": {{ "user.preferred_style": "brief", "project.default_test_command": "pytest -q" }} }} — memory_patch только если пользователь явно просит запомнить настройки (команды тестов, линтера, стиль и т.д.). +Формат каждого элемента actions: {{ "kind": "CREATE_FILE" | "CREATE_DIR" | "UPDATE_FILE" | "DELETE_FILE" | "DELETE_DIR", "path": "относительный/путь", "content": "опционально для CREATE_FILE/UPDATE_FILE" }}. +Создавай программы с нуля (PLAN.md, README.md, Makefile/скрипты, код, тесты) или изменяй существующие файлы. Учитывай всё содержимое файлов при анализе. +{} +{} +Путь проекта: {} +Цель пользователя: {} +{} +Отчёт анализа (JSON): +{} +{} +"#, + design_block, trends_block, path, user_goal, content_block, report_json, extra + ) +} + +const REPAIR_PROMPT: &str = r#" +Верни ТОЛЬКО валидный JSON строго по схеме. Никаких комментариев, пояснений и текста вне JSON. +НЕ добавляй никаких новых полей. Предпочти объект с actions (не массив). +Исправь предыдущий ответ — он не прошёл валидацию. +"#; + +const REPAIR_PROMPT_PLAN_ACTIONS_MUST_BE_EMPTY: &str = r#" +В режиме PLAN actions обязан быть пустым массивом []. +Верни объект с "actions": [] и "summary" (диагноз + план шагов). +"#; + +/// Компилирует JSON Schema для локальной валидации (один раз). +fn compiled_response_schema() -> Option { + let schema: serde_json::Value = serde_json::from_str(include_str!("../../config/llm_response_schema.json")).ok()?; + JSONSchema::options().compile(&schema).ok() +} + +/// Локальная валидация ответа против схемы. Best-effort: если схема не компилируется — пропускаем. +fn validate_json_against_schema(value: &serde_json::Value) -> Result<(), String> { + let Some(compiled) = compiled_response_schema() else { + return Ok(()); // схема не загружена — не валидируем + }; + compiled.validate(value).map_err(|errs| { + let msgs: Vec = errs.map(|e| e.to_string()).collect(); + format!("JSON schema validation failed: {}", msgs.join("; ")) + }) +} + +/// Извлекает JSON из ответа (убирает обёртку ```json ... ``` при наличии). +fn extract_json_from_content(content: &str) -> Result<&str, String> { + let content = content.trim(); + if let Some(start) = content.find("```json") { + let after = &content[start + 7..]; + let end = after + .find("```") + .map(|i| i) + .unwrap_or(after.len()); + Ok(after[..end].trim()) + } else if let Some(start) = content.find("```") { + let after = &content[start + 3..]; + let end = after + .find("```") + .map(|i| i) + .unwrap_or(after.len()); + Ok(after[..end].trim()) + } else { + Ok(content) + } +} + +/// Нормализует path и проверяет запрещённые сегменты. +fn validate_path(path: &str, idx: usize) -> Result<(), String> { + if path.contains('\0') { + return Err(format!("actions[{}].path invalid: contains NUL (ERR_INVALID_PATH)", idx)); + } + if path.chars().any(|c| c.is_control() && c != '\n' && c != '\t') { + return Err(format!( + "actions[{}].path invalid: contains control characters (ERR_INVALID_PATH)", + idx + )); + } + let normalized = path.replace('\\', "/"); + let trimmed = normalized.trim(); + if trimmed.is_empty() || trimmed == "." { + return Err(format!( + "actions[{}].path invalid: path is empty or '.' (ERR_INVALID_PATH)", + idx + )); + } + if trimmed.starts_with('/') || trimmed.starts_with("//") { + return Err(format!( + "actions[{}].path invalid: absolute path not allowed ({}) (ERR_INVALID_PATH)", + idx, path + )); + } + if trimmed.len() >= 2 && trimmed.chars().nth(1) == Some(':') { + return Err(format!( + "actions[{}].path invalid: Windows drive letter not allowed ({}) (ERR_INVALID_PATH)", + idx, path + )); + } + if trimmed.starts_with('~') { + return Err(format!( + "actions[{}].path invalid: tilde not allowed ({}) (ERR_INVALID_PATH)", + idx, path + )); + } + for (seg_i, seg) in trimmed.split('/').enumerate() { + if seg == ".." { + return Err(format!( + "actions[{}].path invalid: '..' segment not allowed ({}) (ERR_INVALID_PATH)", + idx, path + )); + } + if seg == "." && seg_i > 0 { + return Err(format!( + "actions[{}].path invalid: '.' as path segment not allowed ({}) (ERR_INVALID_PATH)", + idx, path + )); + } + } + Ok(()) +} + +/// Проверяет конфликты действий на один path (CREATE+UPDATE, DELETE+UPDATE и т.д.). +fn validate_action_conflicts(actions: &[Action]) -> Result<(), String> { + use std::collections::HashMap; + let mut by_path: HashMap> = HashMap::new(); + for a in actions { + let path = a.path.replace('\\', "/").trim().to_string(); + by_path.entry(path).or_default().push(a.kind.clone()); + } + for (path, kinds) in by_path { + let has_create = kinds.contains(&ActionKind::CreateFile); + let has_update = kinds.contains(&ActionKind::UpdateFile); + let has_delete_file = kinds.contains(&ActionKind::DeleteFile); + let has_delete_dir = kinds.contains(&ActionKind::DeleteDir); + if has_create && has_update { + return Err(format!( + "ERR_ACTION_CONFLICT: path '{}' has both CREATE_FILE and UPDATE_FILE", + path + )); + } + if (has_delete_file || has_delete_dir) && (has_create || has_update) { + return Err(format!( + "ERR_ACTION_CONFLICT: path '{}' has conflicting DELETE and CREATE/UPDATE", + path + )); + } + } + Ok(()) +} + +/// Извлекает пути файлов, прочитанных в plan (FILE[path]: или === path === в plan_context). +fn extract_files_read_from_plan_context(plan_context: &str) -> std::collections::HashSet { + let mut paths = std::collections::HashSet::new(); + let mut search = plan_context; + // FILE[path]: — из fulfill_context_requests + while let Some(start) = search.find("FILE[") { + search = &search[start + 5..]; + if let Some(end) = search.find("]:") { + let path = search[..end].trim().replace('\\', "/"); + if !path.is_empty() { + paths.insert(path); + } + search = &search[end + 2..]; + } else { + break; + } + } + search = plan_context; + // === path === — из project_content + while let Some(start) = search.find("=== ") { + search = &search[start + 4..]; + if let Some(end) = search.find(" ===") { + let path = search[..end].trim().replace('\\', "/"); + if !path.is_empty() && !path.contains('\n') { + paths.insert(path); + } + search = &search[end + 4..]; + } else { + break; + } + } + paths +} + +/// APPLY-режим: каждый UPDATE_FILE должен ссылаться на файл, прочитанный в plan. +fn validate_update_without_base( + actions: &[Action], + plan_context: Option<&str>, +) -> Result<(), String> { + let Some(ctx) = plan_context else { return Ok(()) }; + let read_paths = extract_files_read_from_plan_context(ctx); + for (i, a) in actions.iter().enumerate() { + if a.kind == ActionKind::UpdateFile { + let path = a.path.replace('\\', "/").trim().to_string(); + if !read_paths.contains(&path) { + return Err(format!( + "ERR_UPDATE_WITHOUT_BASE: UPDATE_FILE path '{}' not read in plan (actions[{}]). \ + В PLAN-цикле должен быть context_requests.read_file для этого path.", + path, i + )); + } + } + } + Ok(()) +} + +const MAX_PATH_LEN: usize = 240; +const MAX_ACTIONS: usize = 200; +const MAX_TOTAL_CONTENT_BYTES: usize = 5 * 1024 * 1024; // 5MB +const MAX_CONTENT_NON_PRINTABLE_RATIO: f32 = 0.1; // >10% non-printable = reject + +/// Проверяет content на NUL и pseudo-binary. +fn validate_content(content: &str, idx: usize) -> Result<(), String> { + if content.contains('\0') { + return Err(format!( + "actions[{}].content invalid: contains NUL (ERR_PSEUDO_BINARY)", + idx + )); + } + let len = content.chars().count(); + if len == 0 { + return Ok(()); + } + let non_printable = content + .chars() + .filter(|c| !c.is_ascii_graphic() && *c != '\n' && *c != '\r' && *c != '\t' && *c != ' ') + .count(); + let ratio = non_printable as f32 / len as f32; + if ratio > MAX_CONTENT_NON_PRINTABLE_RATIO { + return Err(format!( + "actions[{}].content invalid: >{}% non-printable (ERR_PSEUDO_BINARY)", + idx, + (MAX_CONTENT_NON_PRINTABLE_RATIO * 100.0) as u32 + )); + } + Ok(()) +} + +/// Валидирует actions: path, content, конфликты, лимиты. +fn validate_actions(actions: &[Action]) -> Result<(), String> { + if actions.len() > MAX_ACTIONS { + return Err(format!( + "ERR_TOO_MANY_ACTIONS: {} > {} (max_actions)", + actions.len(), + MAX_ACTIONS + )); + } + let mut total_bytes = 0usize; + for (i, a) in actions.iter().enumerate() { + validate_path(&a.path, i)?; + if a.path.len() > MAX_PATH_LEN { + return Err(format!( + "actions[{}].path invalid: length {} > {} (ERR_PATH_TOO_LONG)", + i, a.path.len(), MAX_PATH_LEN + )); + } + match a.kind { + ActionKind::CreateFile | ActionKind::UpdateFile => { + let content = a.content.as_ref().map(|s| s.as_str()).unwrap_or(""); + if content.trim().is_empty() { + return Err(format!( + "actions[{}].content required for {} (ERR_CONTENT_REQUIRED)", + i, + match a.kind { + ActionKind::CreateFile => "CREATE_FILE", + ActionKind::UpdateFile => "UPDATE_FILE", + _ => unreachable!(), + } + )); + } + validate_content(content, i)?; + total_bytes += content.len(); + } + _ => {} + } + } + if total_bytes > MAX_TOTAL_CONTENT_BYTES { + return Err(format!( + "ERR_CONTENT_TOO_LARGE: total {} bytes > {} (max_total_bytes)", + total_bytes, MAX_TOTAL_CONTENT_BYTES + )); + } + validate_action_conflicts(actions)?; + Ok(()) +} + +/// Парсит массив действий из JSON; нормализует kind в допустимые значения. +fn parse_actions_from_json(json_str: &str) -> Result, String> { + let raw: Vec = + serde_json::from_str(json_str).map_err(|e| format!("JSON: {}", e))?; + let mut actions = Vec::new(); + for (i, v) in raw.iter().enumerate() { + let obj = v + .as_object() + .ok_or_else(|| format!("action[{}] is not an object", i))?; + let kind_str = obj + .get("kind") + .and_then(|k| k.as_str()) + .unwrap_or("CREATE_FILE"); + let kind = match kind_str.to_uppercase().as_str() { + "CREATE_FILE" => ActionKind::CreateFile, + "CREATE_DIR" => ActionKind::CreateDir, + "UPDATE_FILE" => ActionKind::UpdateFile, + "DELETE_FILE" => ActionKind::DeleteFile, + "DELETE_DIR" => ActionKind::DeleteDir, + _ => ActionKind::CreateFile, + }; + let path = obj + .get("path") + .and_then(|p| p.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("unknown_{}", i)); + let content = obj + .get("content") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()); + actions.push(Action { kind, path, content }); + } + Ok(actions) +} + +/// Результат парсинга ответа LLM: actions, memory_patch, summary (для Fix-plan), context_requests для следующего раунда. +struct PlanParseResult { + actions: Vec, + memory_patch: Option>, + summary_override: Option, + context_requests: Option>, +} + +/// Парсит ответ LLM: массив действий, объект { actions, memory_patch } или Fix-plan { mode, summary, proposed_changes.actions, context_requests, ... }. +fn parse_plan_response(json_str: &str) -> Result { + let value: serde_json::Value = + serde_json::from_str(json_str).map_err(|e| format!("JSON: {}", e))?; + let (actions_value, memory_patch, summary_override, context_requests) = if value.is_array() { + (value, None, None, None) + } else if let Some(obj) = value.as_object() { + let actions_value = obj + .get("proposed_changes") + .and_then(|pc| pc.get("actions").cloned()) + .or_else(|| obj.get("actions").cloned()) + .unwrap_or_else(|| serde_json::Value::Array(vec![])); + let memory_patch = obj.get("memory_patch").and_then(|v| v.as_object()).map(|m| { + m.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>() + }); + let summary_override = obj.get("summary").and_then(|v| v.as_str()).map(String::from); + let context_requests = obj.get("context_requests").and_then(|v| v.as_array()).map(|a| { + a.iter().cloned().collect::>() + }); + (actions_value, memory_patch, summary_override, context_requests) + } else { + return Err("expected JSON array or object with 'actions'".into()); + }; + let actions_str = serde_json::to_string(&actions_value).map_err(|e| e.to_string())?; + let actions = parse_actions_from_json(&actions_str)?; + Ok(PlanParseResult { + actions, + memory_patch, + summary_override, + context_requests, + }) +} + +const MAX_CONTEXT_ROUNDS: u32 = 2; + +/// Вызывает LLM API и возвращает план (AgentPlan). +/// Автосбор контекста: env + project prefs в начало user message; при context_requests — до MAX_CONTEXT_ROUNDS раундов. +/// output_format_override: "plan" | "apply" — для двухфазного Plan→Apply. +/// last_plan_for_apply, last_context_for_apply: при переходе из Plan в Apply (user сказал "ok"). +const DEFAULT_MAX_TOKENS: u32 = 16384; + +pub async fn plan( + user_prefs_path: &Path, + project_prefs_path: &Path, + path: &str, + report_json: &str, + user_goal: &str, + project_content: Option<&str>, + design_style: Option<&str>, + trends_context: Option<&str>, + output_format_override: Option<&str>, + last_plan_for_apply: Option<&str>, + last_context_for_apply: Option<&str>, +) -> Result { + let trace_id = Uuid::new_v4().to_string(); + + let api_url = std::env::var("PAPAYU_LLM_API_URL").map_err(|_| "PAPAYU_LLM_API_URL not set")?; + let api_url = api_url.trim(); + if api_url.is_empty() { + return Err("PAPAYU_LLM_API_URL is empty".into()); + } + + let model = std::env::var("PAPAYU_LLM_MODEL") + .unwrap_or_else(|_| "gpt-4o-mini".to_string()); + let api_key = std::env::var("PAPAYU_LLM_API_KEY").ok(); + + let mem = memory::load_memory(user_prefs_path, project_prefs_path); + let mut memory_block = memory::build_memory_block(&mem); + // Переопределение режима для Plan→Apply + if let Some(of) = output_format_override { + if of == "plan" || of == "apply" { + memory_block.push_str(&format!("\n\nРЕЖИМ_ДЛЯ_ЭТОГО_ЗАПРОСА: {} (соблюдай строго)", of)); + } + } + let system_prompt = get_system_prompt_for_mode(); + let system_content = format!("{}{}\n\nLLM_PLAN_SCHEMA_VERSION={}", system_prompt, memory_block, LLM_PLAN_SCHEMA_VERSION); + + let project_root = Path::new(path); + let base_context = context::gather_base_context(project_root, &mem); + let prompt_body = build_prompt(path, report_json, user_goal, project_content, design_style, trends_context); + // Эвристики автосбора: Traceback, ImportError и т.д. + let auto_from_message = context::gather_auto_context_from_message( + project_root, + &format!("{}\n{}", user_goal, report_json), + ); + let mut user_message = format!("{}{}{}", base_context, prompt_body, auto_from_message); + + // Переход Plan→Apply: инжектируем сохранённый план и контекст + if output_format_override == Some("apply") { + if let Some(plan_json) = last_plan_for_apply { + let mut apply_prompt = String::from("\n\n--- РЕЖИМ APPLY ---\nПользователь подтвердил план. Применяй изменения согласно плану ниже. Верни actions с конкретными правками файлов.\n\nПЛАН:\n"); + apply_prompt.push_str(plan_json); + if let Some(ctx) = last_context_for_apply { + apply_prompt.push_str("\n\nСОБРАННЫЙ_КОНТЕКСТ:\n"); + apply_prompt.push_str(ctx); + } + user_message.push_str(&apply_prompt); + } + } + + let timeout_sec = std::env::var("PAPAYU_LLM_TIMEOUT_SEC") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(90); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(timeout_sec)) + .build() + .map_err(|e| format!("HTTP client: {}", e))?; + + let mut round = 0u32; + + let use_strict_json = std::env::var("PAPAYU_LLM_STRICT_JSON") + .map(|s| matches!(s.trim().to_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + + let temperature = std::env::var("PAPAYU_LLM_TEMPERATURE") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0.0); + + let input_chars = system_content.len() + user_message.len(); + let configured_max_tokens = std::env::var("PAPAYU_LLM_MAX_TOKENS") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(DEFAULT_MAX_TOKENS); + let max_tokens = if input_chars > INPUT_CHARS_FOR_CAP { + configured_max_tokens.min(MAX_TOKENS_WHEN_LARGE_INPUT) + } else { + configured_max_tokens + }; + + let provider = api_url + .split('/') + .nth(2) + .unwrap_or("unknown") + .split(':') + .next() + .unwrap_or("unknown"); + + let response_format = if use_strict_json { + let schema_json: serde_json::Value = serde_json::from_str(include_str!("../../config/llm_response_schema.json")) + .unwrap_or_else(|_| serde_json::json!({})); + Some(ResponseFormatJsonSchema { + ty: "json_schema".to_string(), + json_schema: ResponseFormatJsonSchemaInner { + name: "papa_yu_response".to_string(), + schema: schema_json, + strict: true, + }, + }) + } else { + None + }; + + let mut repair_done = false; + let mut skip_response_format = false; // capability detection: fallback при ошибке response_format + let mut context_cache = context::ContextCache::new(); + + let (last_actions, last_summary_override, last_plan_json, last_context_for_return) = loop { + let effective_response_format = if skip_response_format { + None + } else { + response_format.clone() + }; + + let body = ChatRequest { + model: model.trim().to_string(), + messages: vec![ + ChatMessage { + role: "system".to_string(), + content: system_content.clone(), + }, + ChatMessage { + role: "user".to_string(), + content: user_message.clone(), + }, + ], + temperature: Some(temperature), + max_tokens: Some(max_tokens), + top_p: Some(1.0), + presence_penalty: Some(0.0), + frequency_penalty: Some(0.0), + response_format: effective_response_format, + }; + + log_llm_event( + &trace_id, + "LLM_REQUEST_SENT", + &[ + ("model", model.trim().to_string()), + ("schema_version", LLM_PLAN_SCHEMA_VERSION.to_string()), + ("strict_json", (!skip_response_format && use_strict_json).to_string()), + ("provider", provider.to_string()), + ("token_budget", max_tokens.to_string()), + ("input_chars", input_chars.to_string()), + ], + ); + + let mut req = client.post(api_url).json(&body); + if let Some(key) = &api_key { + if !key.trim().is_empty() { + req = req.header("Authorization", format!("Bearer {}", key.trim())); + } + } + + let resp = req.send().await.map_err(|e| { + if e.is_timeout() { + log_llm_event(&trace_id, "LLM_REQUEST_TIMEOUT", &[("timeout_sec", timeout_sec.to_string())]); + } + format!("Request: {}", e) + })?; + let status = resp.status(); + let text = resp.text().await.map_err(|e| format!("Response body: {}", e))?; + + if !status.is_success() { + // Capability detection: если strict_json и ошибка — возможно response_format не поддерживается + if use_strict_json && !skip_response_format { + let lower = text.to_lowercase(); + if lower.contains("response_format") + || lower.contains("json_schema") + || lower.contains("unknown field") + || lower.contains("not supported") + { + skip_response_format = true; + log_llm_event( + &trace_id, + "LLM_RESPONSE_FORMAT_FALLBACK", + &[ + ("reason", "provider_error".to_string()), + ("status", status.as_str().to_string()), + ], + ); + continue; + } + } + return Err(format!("API error {}: {}", status, text)); + } + + log_llm_event( + &trace_id, + if repair_done { "LLM_RESPONSE_REPAIR_RETRY" } else { "LLM_RESPONSE_OK" }, + &[("round", round.to_string())], + ); + + let chat: ChatResponse = + serde_json::from_str(&text).map_err(|e| format!("Response JSON: {}", e))?; + let content = chat + .choices + .as_ref() + .and_then(|c| c.first()) + .and_then(|c| c.message.content.as_deref()) + .ok_or_else(|| "No choices in API response".to_string())?; + + // Парсинг JSON: best-effort (извлечь из markdown при наличии) + let json_str = match extract_json_from_content(content) { + Ok(s) => s, + Err(e) if !repair_done => { + log_llm_event(&trace_id, "VALIDATION_FAILED", &[("code", "ERR_JSON_EXTRACT".to_string()), ("reason", e.clone())]); + user_message.push_str(&format!( + "\n\n---\n{REPAIR_PROMPT}\n\nRaw output:\n{content}" + )); + repair_done = true; + continue; + } + Err(e) => { + let mut trace_val = serde_json::json!({ "trace_id": trace_id, "raw_content": content, "error": e, "event": "VALIDATION_FAILED" }); + write_trace(path, &trace_id, &mut trace_val); + return Err(e); + } + }; + + // Десериализация в Value + let value: serde_json::Value = match serde_json::from_str(json_str) { + Ok(v) => v, + Err(e) if !repair_done => { + user_message.push_str(&format!( + "\n\n---\nERR_JSON_PARSE: {}\n\n{REPAIR_PROMPT}\n\nRaw output:\n{content}", + e + )); + repair_done = true; + continue; + } + Err(e) => return Err(format!("JSON parse: {}", e)), + }; + + // Локальная валидация схемы (best-effort при strict выкл; обязательна при strict вкл) + if let Err(e) = validate_json_against_schema(&value) { + log_llm_event(&trace_id, "VALIDATION_FAILED", &[("code", "ERR_SCHEMA_VALIDATION".to_string()), ("reason", e.clone())]); + if !repair_done { + user_message.push_str(&format!( + "\n\n---\nERR_SCHEMA_VALIDATION: {}\n\n{REPAIR_PROMPT}\n\nRaw output:\n{content}", + e + )); + repair_done = true; + continue; + } + let mut trace_val = serde_json::json!({ "trace_id": trace_id, "raw_content": content, "validated_json": json_str, "error": e, "event": "VALIDATION_FAILED" }); + write_trace(path, &trace_id, &mut trace_val); + return Err(e); + } + + let parsed = parse_plan_response(json_str)?; + + // Жёсткая валидация режимов: PLAN → actions=[], APPLY → actions непустой (если нужны изменения) + let mode: &str = output_format_override.unwrap_or_else(|| { + let s = mem.user.output_format.trim(); + if s.is_empty() { "" } else { mem.user.output_format.as_str() } + }); + if mode == "plan" && !parsed.actions.is_empty() { + if !repair_done { + user_message.push_str(&format!( + "\n\n---\n{REPAIR_PROMPT_PLAN_ACTIONS_MUST_BE_EMPTY}\n\nRaw output:\n{content}" + )); + repair_done = true; + continue; + } + return Err("В режиме PLAN actions обязан быть []".to_string()); + } + if mode == "apply" && parsed.actions.is_empty() { + let summary = parsed.summary_override.as_deref().unwrap_or(""); + let no_changes = summary.trim().starts_with("NO_CHANGES:"); + if !no_changes && !repair_done { + user_message.push_str(&format!( + "\n\n---\nERR_APPLY_EMPTY_ACTIONS: В режиме APPLY при пустом actions summary обязан начинаться с \"NO_CHANGES:\". Raw output:\n{content}" + )); + repair_done = true; + continue; + } + if !no_changes { + return Err("В режиме APPLY при пустом actions summary обязан начинаться с NO_CHANGES:".to_string()); + } + } + + // PAPAYU_MEMORY_AUTOPATCH=1 — применять memory_patch; иначе игнорировать (только по явному согласию) + let autopatch = std::env::var("PAPAYU_MEMORY_AUTOPATCH") + .map(|s| matches!(s.trim().to_lowercase().as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + if autopatch { + if let Some(patch) = &parsed.memory_patch { + let (new_user, new_project) = + memory::apply_memory_patch(patch, &mem.user, &mem.project); + let _ = memory::save_user_prefs(user_prefs_path, &new_user); + let _ = memory::save_project_prefs(project_prefs_path, &new_project); + } + } + + let context_requests = parsed.context_requests.as_deref().unwrap_or(&[]); + if !context_requests.is_empty() && round < MAX_CONTEXT_ROUNDS { + let fulfilled = context::fulfill_context_requests( + project_root, + context_requests, + 200, + Some(&mut context_cache), + Some(&trace_id), + ); + user_message.push_str(&fulfilled); + round += 1; + continue; + } + + break (parsed.actions, parsed.summary_override, json_str.to_string(), user_message.clone()); + }; + + // Строгая валидация: path, content, конфликты, UPDATE_WITHOUT_BASE + if let Err(e) = validate_actions(&last_actions) { + log_llm_event(&trace_id, "VALIDATION_FAILED", &[("code", "ERR_ACTIONS".to_string()), ("reason", e.clone())]); + let mut trace_val = serde_json::json!({ "trace_id": trace_id, "validated_json": last_plan_json, "error": e, "event": "VALIDATION_FAILED" }); + write_trace(path, &trace_id, &mut trace_val); + return Err(e); + } + let mode_for_update_base = output_format_override + .filter(|s| !s.is_empty()) + .or_else(|| if mem.user.output_format.trim().is_empty() { None } else { Some(mem.user.output_format.as_str()) }); + if mode_for_update_base == Some("apply") { + if let Err(e) = validate_update_without_base(&last_actions, last_context_for_apply) { + log_llm_event(&trace_id, "VALIDATION_FAILED", &[("code", "ERR_UPDATE_WITHOUT_BASE".to_string()), ("reason", e.clone())]); + let mut trace_val = serde_json::json!({ "trace_id": trace_id, "validated_json": last_plan_json, "error": e, "event": "VALIDATION_FAILED" }); + write_trace(path, &trace_id, &mut trace_val); + return Err(e); + } + } + + let mode_for_plan_json = output_format_override + .filter(|s| !s.is_empty()) + .or_else(|| if mem.user.output_format.is_empty() { None } else { Some(mem.user.output_format.as_str()) }); + let is_plan_mode = mode_for_plan_json == Some("plan"); + let plan_json = is_plan_mode.then_some(last_plan_json.clone()); + let plan_context = is_plan_mode.then_some(last_context_for_return.clone()); + + let mut trace_val = serde_json::json!({ + "trace_id": trace_id, + "event": "LLM_PLAN_OK", + "schema_version": LLM_PLAN_SCHEMA_VERSION, + "model": model.trim(), + "provider": provider, + "actions_count": last_actions.len(), + "validated_json": last_plan_json, + }); + write_trace(path, &trace_id, &mut trace_val); + + Ok(AgentPlan { + ok: true, + summary: last_summary_override + .unwrap_or_else(|| format!("План от LLM: {} действий.", last_actions.len())), + actions: last_actions, + error: None, + error_code: None, + plan_json, + plan_context, + }) +} + +#[cfg(test)] +mod tests { + use super::{ + extract_files_read_from_plan_context, parse_actions_from_json, schema_hash, validate_actions, + validate_update_without_base, FIX_PLAN_SYSTEM_PROMPT, LLM_PLAN_SCHEMA_VERSION, + }; + use crate::types::{Action, ActionKind}; + + #[test] + fn test_schema_version_is_one() { + assert_eq!(LLM_PLAN_SCHEMA_VERSION, 1); + } + + #[test] + fn test_schema_hash_non_empty() { + let h = schema_hash(); + assert!(!h.is_empty()); + assert_eq!(h.len(), 64); // sha256 hex + } + + #[test] + fn test_system_prompt_contains_schema_version() { + let system_content = format!( + "{}\n\nLLM_PLAN_SCHEMA_VERSION={}", + FIX_PLAN_SYSTEM_PROMPT, LLM_PLAN_SCHEMA_VERSION + ); + assert!(system_content.contains("LLM_PLAN_SCHEMA_VERSION=1")); + } + + #[test] + fn test_validate_actions_empty() { + assert!(validate_actions(&[]).is_ok()); + } + + #[test] + fn test_validate_actions_valid_create_file() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "README.md".to_string(), + content: Some("# Project".to_string()), + }]; + assert!(validate_actions(&actions).is_ok()); + } + + #[test] + fn test_validate_actions_rejects_parent_path() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "../etc/passwd".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_absolute_path() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "/etc/passwd".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_path_ending_with_dotdot() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "a/..".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_windows_drive() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "C:/foo/bar".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_unc_path() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "//server/share/file".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_dot_path() { + let actions = vec![Action { + kind: ActionKind::CreateDir, + path: ".".to_string(), + content: None, + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_dot_segment() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "a/./b".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_allows_relative_prefix() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "./src/main.rs".to_string(), + content: Some("fn main() {}".to_string()), + }]; + assert!(validate_actions(&actions).is_ok()); + } + + #[test] + fn test_validate_actions_rejects_conflict_create_update() { + let actions = vec![ + Action { + kind: ActionKind::CreateFile, + path: "foo.txt".to_string(), + content: Some("a".to_string()), + }, + Action { + kind: ActionKind::UpdateFile, + path: "foo.txt".to_string(), + content: Some("b".to_string()), + }, + ]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_rejects_conflict_delete_update() { + let actions = vec![ + Action { + kind: ActionKind::DeleteFile, + path: "foo.txt".to_string(), + content: None, + }, + Action { + kind: ActionKind::UpdateFile, + path: "foo.txt".to_string(), + content: Some("b".to_string()), + }, + ]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_extract_files_from_plan_context() { + let ctx = "FILE[src/main.rs]:\nfn main() {}\n\n=== README.md ===\n# Project\n"; + let paths = extract_files_read_from_plan_context(ctx); + assert!(paths.contains("src/main.rs")); + assert!(paths.contains("README.md")); + } + + #[test] + fn test_validate_update_without_base_ok() { + let ctx = "FILE[foo.txt]:\nold\n\n=== bar.txt ===\ncontent\n"; + let actions = vec![ + Action { + kind: ActionKind::UpdateFile, + path: "foo.txt".to_string(), + content: Some("new".to_string()), + }, + Action { + kind: ActionKind::UpdateFile, + path: "bar.txt".to_string(), + content: Some("updated".to_string()), + }, + ]; + assert!(validate_update_without_base(&actions, Some(ctx)).is_ok()); + } + + #[test] + fn test_validate_update_without_base_err() { + let ctx = "FILE[foo.txt]:\nold\n"; + let actions = vec![Action { + kind: ActionKind::UpdateFile, + path: "unknown.txt".to_string(), + content: Some("new".to_string()), + }]; + assert!(validate_update_without_base(&actions, Some(ctx)).is_err()); + } + + #[test] + fn test_validate_actions_rejects_tilde_path() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "~/etc/passwd".to_string(), + content: Some("x".to_string()), + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_validate_actions_requires_content_for_create_file() { + let actions = vec![Action { + kind: ActionKind::CreateFile, + path: "README.md".to_string(), + content: None, + }]; + assert!(validate_actions(&actions).is_err()); + } + + #[test] + fn test_parse_actions_from_json_array() { + let json = r#"[{"kind":"CREATE_FILE","path":"a.txt","content":"x"}]"#; + let actions = parse_actions_from_json(json).unwrap(); + assert_eq!(actions.len(), 1); + assert_eq!(actions[0].path, "a.txt"); + } + + #[test] + fn test_parse_actions_from_json_object() { + let json = r#"{"actions":[{"kind":"CREATE_DIR","path":"src"}]}"#; + let raw: serde_json::Value = serde_json::from_str(json).unwrap(); + let actions_value = raw.get("actions").cloned().unwrap(); + let actions_str = serde_json::to_string(&actions_value).unwrap(); + let actions = parse_actions_from_json(&actions_str).unwrap(); + assert_eq!(actions.len(), 1); + assert_eq!(actions[0].path, "src"); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..ced25a3 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,40 @@ +mod agentic_run; +mod analyze_project; +mod apply_actions; +mod apply_actions_tx; +mod auto_check; +mod folder_links; +mod generate_actions; +mod generate_actions_from_report; +mod get_project_profile; +mod llm_planner; +mod preview_actions; +mod project_content; +mod projects; +mod propose_actions; +mod redo_last; +mod run_batch; +mod settings_export; +mod trends; +mod undo_last; +mod undo_last_tx; +mod undo_status; + +pub use agentic_run::agentic_run; +pub use get_project_profile::get_project_profile; +pub use projects::{add_project, append_session_event, get_project_settings, list_projects, list_sessions, set_project_settings}; +pub use analyze_project::analyze_project; +pub use apply_actions::apply_actions; +pub use apply_actions_tx::apply_actions_tx; +pub use generate_actions::generate_actions; +pub use generate_actions_from_report::generate_actions_from_report; +pub use propose_actions::propose_actions; +pub use folder_links::{load_folder_links, save_folder_links, FolderLinks}; +pub use preview_actions::preview_actions; +pub use redo_last::redo_last; +pub use run_batch::run_batch; +pub use trends::{fetch_trends_recommendations, get_trends_recommendations}; +pub use undo_last::{get_undo_redo_state_cmd, undo_available, undo_last}; +pub use undo_last_tx::undo_last_tx; +pub use undo_status::undo_status; +pub use settings_export::{export_settings, import_settings}; diff --git a/src-tauri/src/commands/preview_actions.rs b/src-tauri/src/commands/preview_actions.rs new file mode 100644 index 0000000..f462a02 --- /dev/null +++ b/src-tauri/src/commands/preview_actions.rs @@ -0,0 +1,141 @@ +use crate::tx::safe_join; +use crate::types::{ActionKind, ApplyPayload, DiffItem, PreviewResult}; +use std::fs; + +const MAX_PREVIEW_SIZE: usize = 200_000; + +pub fn preview_actions(payload: ApplyPayload) -> Result { + let root = std::path::Path::new(&payload.root_path); + let mut diffs = Vec::new(); + for a in &payload.actions { + let rel = a.path.as_str(); + if is_protected_file(rel) || !is_text_allowed(rel) { + diffs.push(DiffItem { + kind: "blocked".to_string(), + path: a.path.clone(), + old_content: Some("(blocked)".to_string()), + new_content: Some("(blocked)".to_string()), + summary: Some("BLOCKED: protected or non-text file".to_string()), + }); + continue; + } + let item = match &a.kind { + ActionKind::CreateFile => DiffItem { + kind: "create".to_string(), + path: a.path.clone(), + old_content: None, + new_content: a.content.clone(), + summary: None, + }, + ActionKind::CreateDir => DiffItem { + kind: "mkdir".to_string(), + path: a.path.clone(), + old_content: None, + new_content: None, + summary: None, + }, + ActionKind::UpdateFile => { + let old = read_text_if_exists(root, &a.path); + DiffItem { + kind: "update".to_string(), + path: a.path.clone(), + old_content: old, + new_content: a.content.clone(), + summary: None, + } + } + ActionKind::DeleteFile => { + let old = read_text_if_exists(root, &a.path); + DiffItem { + kind: "delete".to_string(), + path: a.path.clone(), + old_content: old, + new_content: None, + summary: None, + } + } + ActionKind::DeleteDir => DiffItem { + kind: "rmdir".to_string(), + path: a.path.clone(), + old_content: None, + new_content: None, + summary: None, + }, + }; + diffs.push(item); + } + let summary = summarize(&diffs); + let files = diffs.len(); + let bytes = diffs + .iter() + .map(|d| d.old_content.as_ref().unwrap_or(&String::new()).len() + d.new_content.as_ref().unwrap_or(&String::new()).len()) + .sum::(); + eprintln!("[PREVIEW_READY] path={} files={} diffs={} bytes={}", payload.root_path, files, diffs.len(), bytes); + Ok(PreviewResult { diffs, summary }) +} + +fn read_text_if_exists(root: &std::path::Path, rel: &str) -> Option { + let p = safe_join(root, rel).ok()?; + if !p.is_file() { + return None; + } + let s = fs::read_to_string(&p).ok()?; + if s.len() > MAX_PREVIEW_SIZE { + Some(format!("{}... (truncated)", &s[..MAX_PREVIEW_SIZE])) + } else { + Some(s) + } +} + +fn summarize(diffs: &[DiffItem]) -> String { + let create = diffs.iter().filter(|d| d.kind == "create").count(); + let update = diffs.iter().filter(|d| d.kind == "update").count(); + let delete = diffs.iter().filter(|d| d.kind == "delete").count(); + let mkdir = diffs.iter().filter(|d| d.kind == "mkdir").count(); + let rmdir = diffs.iter().filter(|d| d.kind == "rmdir").count(); + let blocked = diffs.iter().filter(|d| d.kind == "blocked").count(); + let mut s = format!( + "Создать: {}, изменить: {}, удалить: {}, mkdir: {}, rmdir: {}", + create, update, delete, mkdir, rmdir + ); + if blocked > 0 { + s.push_str(&format!(", заблокировано: {}", blocked)); + } + s +} + +fn is_protected_file(p: &str) -> bool { + let lower = p.to_lowercase().replace('\\', "/"); + if lower == ".env" || lower.ends_with("/.env") { return true; } + if lower.ends_with(".pem") || lower.ends_with(".key") || lower.ends_with(".p12") { return true; } + if lower.contains("id_rsa") { return true; } + if lower.contains("/secrets/") || lower.starts_with("secrets/") { return true; } + if lower.ends_with("cargo.lock") { return true; } + if lower.ends_with("package-lock.json") { return true; } + if lower.ends_with("pnpm-lock.yaml") { return true; } + if lower.ends_with("yarn.lock") { return true; } + if lower.ends_with("composer.lock") { return true; } + if lower.ends_with("poetry.lock") { return true; } + if lower.ends_with("pipfile.lock") { return true; } + let bin_ext = [ + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", + ".pdf", ".zip", ".7z", ".rar", ".dmg", ".pkg", + ".exe", ".dll", ".so", ".dylib", ".bin", + ".mp3", ".mp4", ".mov", ".avi", + ".wasm", ".class", + ]; + for ext in bin_ext { + if lower.ends_with(ext) { return true; } + } + false +} + +fn is_text_allowed(p: &str) -> bool { + let lower = p.to_lowercase(); + let ok_ext = [ + ".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".toml", ".yaml", ".yml", + ".rs", ".py", ".go", ".java", ".kt", ".c", ".cpp", ".h", ".hpp", + ".css", ".scss", ".html", ".env", ".gitignore", ".editorconfig", + ]; + ok_ext.iter().any(|e| lower.ends_with(e)) || !lower.contains('.') +} diff --git a/src-tauri/src/commands/project_content.rs b/src-tauri/src/commands/project_content.rs new file mode 100644 index 0000000..40ec01d --- /dev/null +++ b/src-tauri/src/commands/project_content.rs @@ -0,0 +1,145 @@ +//! Сбор полного содержимого проекта для ИИ: все релевантные файлы/папки в пределах лимитов. +//! Анализ ИИ-агентом делается по всему содержимому, а не по трём файлам. + +use std::fs; +use std::path::Path; + +/// Расширения текстовых файлов для включения в контекст ИИ +const TEXT_EXT: &[&str] = &[ + "ts", "tsx", "js", "jsx", "mjs", "cjs", "rs", "py", "json", "toml", "md", "yml", "yaml", + "css", "scss", "html", "xml", "vue", "svelte", "go", "rb", "java", "kt", "swift", "c", "h", + "cpp", "hpp", "sh", "bash", "zsh", "sql", "graphql", +]; + +/// Папки, которые не сканируем +const EXCLUDE_DIRS: &[&str] = &[ + "node_modules", "target", "dist", "build", ".git", ".next", ".nuxt", ".cache", + "coverage", "__pycache__", ".venv", "venv", ".idea", ".vscode", "vendor", +]; + +/// Макс. символов на файл (чтобы не перегружать контекст) +const MAX_BYTES_PER_FILE: usize = 80_000; +/// Макс. суммарных символов для контекста LLM (~200k токенов) +const MAX_TOTAL_CHARS: usize = 600_000; +/// Макс. число файлов +const MAX_FILES: usize = 500; + +/// Собирает содержимое релевантных файлов проекта в одну строку для передачи в LLM. +/// Сканирует всю папку/папки (без искусственного ограничения «тремя файлами»). +pub fn get_project_content_for_llm(root: &Path, max_total_chars: Option) -> String { + let limit = max_total_chars.unwrap_or(MAX_TOTAL_CHARS); + let mut out = String::with_capacity(limit.min(MAX_TOTAL_CHARS + 1024)); + let mut total = 0usize; + let mut files_added = 0usize; + + if !root.exists() || !root.is_dir() { + return "Папка не найдена или пуста. Можно создать проект с нуля.".to_string(); + } + + if let Ok(entries) = fs::read_dir(root) { + for entry in entries.flatten() { + if total >= limit || files_added >= MAX_FILES { + break; + } + let path = entry.path(); + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if path.is_dir() { + if EXCLUDE_DIRS.contains(&name) { + continue; + } + collect_dir(&path, root, &mut out, &mut total, &mut files_added, limit); + } else if path.is_file() { + if let Some(ext) = path.extension() { + let ext = ext.to_str().unwrap_or("").to_lowercase(); + if TEXT_EXT.iter().any(|e| *e == ext) { + if let Ok(content) = fs::read_to_string(&path) { + let rel = path.strip_prefix(root).unwrap_or(&path); + let rel_str = rel.display().to_string(); + let truncated = if content.len() > MAX_BYTES_PER_FILE { + format!("{}…\n(обрезано, всего {} байт)", &content[..MAX_BYTES_PER_FILE], content.len()) + } else { + content + }; + let block = format!("\n=== {} ===\n{}\n", rel_str, truncated); + if total + block.len() <= limit { + out.push_str(&block); + total += block.len(); + files_added += 1; + } + } + } + } + } + } + } + + if out.is_empty() { + out = "В папке нет релевантных исходных файлов. Можно создать проект с нуля.".to_string(); + } else { + out.insert_str(0, "Содержимое файлов проекта (полный контекст для анализа):\n"); + } + out +} + +fn collect_dir( + dir: &Path, + root: &Path, + out: &mut String, + total: &mut usize, + files_added: &mut usize, + limit: usize, +) { + if *total >= limit || *files_added >= MAX_FILES { + return; + } + let read = match fs::read_dir(dir) { + Ok(r) => r, + Err(_) => return, + }; + let mut entries: Vec<_> = read.flatten().collect(); + entries.sort_by(|a, b| { + let a = a.path(); + let b = b.path(); + let a_dir = a.is_dir(); + let b_dir = b.is_dir(); + match (a_dir, b_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.file_name().cmp(&b.file_name()), + } + }); + for entry in entries { + if *total >= limit || *files_added >= MAX_FILES { + break; + } + let path = entry.path(); + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if path.is_dir() { + if EXCLUDE_DIRS.contains(&name) { + continue; + } + collect_dir(&path, root, out, total, files_added, limit); + } else if path.is_file() { + if let Some(ext) = path.extension() { + let ext = ext.to_str().unwrap_or("").to_lowercase(); + if TEXT_EXT.iter().any(|e| *e == ext) { + if let Ok(content) = fs::read_to_string(&path) { + let rel = path.strip_prefix(root).unwrap_or(&path); + let rel_str = rel.display().to_string(); + let truncated = if content.len() > MAX_BYTES_PER_FILE { + format!("{}…\n(обрезано, всего {} байт)", &content[..MAX_BYTES_PER_FILE], content.len()) + } else { + content + }; + let block = format!("\n=== {} ===\n{}\n", rel_str, truncated); + if *total + block.len() <= limit { + out.push_str(&block); + *total += block.len(); + *files_added += 1; + } + } + } + } + } + } +} diff --git a/src-tauri/src/commands/projects.rs b/src-tauri/src/commands/projects.rs new file mode 100644 index 0000000..5ff26cc --- /dev/null +++ b/src-tauri/src/commands/projects.rs @@ -0,0 +1,102 @@ +//! v2.5: Projects & sessions — list/add projects, profiles, session history. + +use crate::store::{ + add_session_event as store_add_session_event, load_profiles, load_projects, load_sessions, + save_profiles, save_projects, +}; +use crate::types::{Project, ProjectSettings, Session, SessionEvent}; +use tauri::Manager; + +fn app_data_dir(app: &tauri::AppHandle) -> Result { + app.path() + .app_data_dir() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn list_projects(app: tauri::AppHandle) -> Result, String> { + let dir = app_data_dir(&app)?; + Ok(load_projects(&dir)) +} + +#[tauri::command] +pub fn add_project(app: tauri::AppHandle, path: String, name: Option) -> Result { + let dir = app_data_dir(&app)?; + let mut projects = load_projects(&dir); + let name = name.unwrap_or_else(|| { + std::path::Path::new(&path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Project") + .to_string() + }); + let id = uuid::Uuid::new_v4().to_string(); + let created_at = chrono::Utc::now().to_rfc3339(); + let project = Project { + id: id.clone(), + path: path.clone(), + name, + created_at: created_at.clone(), + }; + if projects.iter().any(|p| p.path == path) { + return Err("Project with this path already exists".to_string()); + } + projects.push(project.clone()); + save_projects(&dir, &projects)?; + Ok(project) +} + +#[tauri::command] +pub fn get_project_settings(app: tauri::AppHandle, project_id: String) -> Result { + let dir = app_data_dir(&app)?; + let profiles = load_profiles(&dir); + Ok(profiles + .get(&project_id) + .cloned() + .unwrap_or_else(|| ProjectSettings { + project_id: project_id.clone(), + auto_check: true, + max_attempts: 2, + max_actions: 12, + goal_template: None, + })) +} + +#[tauri::command] +pub fn set_project_settings(app: tauri::AppHandle, profile: ProjectSettings) -> Result<(), String> { + let dir = app_data_dir(&app)?; + let mut profiles = load_profiles(&dir); + profiles.insert(profile.project_id.clone(), profile); + save_profiles(&dir, &profiles)?; + Ok(()) +} + +#[tauri::command] +pub fn list_sessions(app: tauri::AppHandle, project_id: Option) -> Result, String> { + let dir = app_data_dir(&app)?; + let mut sessions = load_sessions(&dir); + sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + if let Some(pid) = project_id { + sessions.retain(|s| s.project_id == pid); + } + Ok(sessions) +} + +#[tauri::command] +pub fn append_session_event( + app: tauri::AppHandle, + project_id: String, + kind: String, + role: Option, + text: Option, +) -> Result { + let dir = app_data_dir(&app)?; + let at = chrono::Utc::now().to_rfc3339(); + let event = SessionEvent { + kind, + role, + text, + at, + }; + store_add_session_event(&dir, &project_id, event) +} diff --git a/src-tauri/src/commands/propose_actions.rs b/src-tauri/src/commands/propose_actions.rs new file mode 100644 index 0000000..5a39c69 --- /dev/null +++ b/src-tauri/src/commands/propose_actions.rs @@ -0,0 +1,249 @@ +//! v3.0: агент предложения исправлений (эвристика или LLM по конфигу). +//! Для LLM передаётся полное содержимое проекта (все файлы), не только отчёт. +//! Инженерная память: user prefs (app_data/papa-yu/preferences.json), project prefs (.papa-yu/project.json). + +use std::path::Path; + +use crate::types::{Action, ActionKind, AgentPlan}; +use tauri::Manager; + +use super::llm_planner; +use super::project_content; + +fn has_readme(root: &str) -> bool { + ["README.md", "README.MD", "README.txt", "README"] + .iter() + .any(|f| Path::new(root).join(f).exists()) +} + +fn has_gitignore(root: &str) -> bool { + Path::new(root).join(".gitignore").exists() +} + +fn has_license(root: &str) -> bool { + ["LICENSE", "LICENSE.md", "LICENSE.txt"] + .iter() + .any(|f| Path::new(root).join(f).exists()) +} + +/// Триггеры перехода Plan→Apply (пользователь подтвердил план). +const APPLY_TRIGGERS: &[&str] = &[ + "ok", "ок", "apply", "применяй", "применить", "делай", "да", "yes", "go", "вперёд", +]; + +#[tauri::command] +pub async fn propose_actions( + app: tauri::AppHandle, + path: String, + report_json: String, + user_goal: String, + design_style: Option, + trends_context: Option, + last_plan_json: Option, + last_context: Option, +) -> AgentPlan { + let goal_trim = user_goal.trim(); + let goal_lower = goal_trim.to_lowercase(); + let root = Path::new(&path); + if !root.exists() || !root.is_dir() { + return AgentPlan { + ok: false, + summary: String::new(), + actions: vec![], + error: Some("path not found".into()), + error_code: Some("PATH_NOT_FOUND".into()), + plan_json: None, + plan_context: None, + }; + } + + if llm_planner::is_llm_configured() { + let app_data = match app.path().app_data_dir() { + Ok(d) => d, + Err(e) => { + return AgentPlan { + ok: false, + summary: String::new(), + actions: vec![], + error: Some(format!("app data dir: {}", e)), + error_code: Some("APP_DATA_DIR".into()), + plan_json: None, + plan_context: None, + }; + } + }; + let user_prefs_path = app_data.join("papa-yu").join("preferences.json"); + let project_prefs_path = root.join(".papa-yu").join("project.json"); + + let full_content = project_content::get_project_content_for_llm(root, None); + let content_for_plan = if full_content.is_empty() { + None + } else { + Some(full_content.as_str()) + }; + let design_ref = design_style.as_deref(); + let trends_ref = trends_context.as_deref(); + + // Определение режима: префиксы plan:/apply:, триггер "ok/применяй" + last_plan, или по умолчанию + let output_format_override: Option<&str> = if goal_lower.starts_with("plan:") { + Some("plan") + } else if goal_lower.starts_with("apply:") { + Some("apply") + } else if APPLY_TRIGGERS.contains(&goal_lower.as_str()) && last_plan_json.is_some() { + Some("apply") + } else if goal_lower.contains("исправь") || goal_lower.contains("почини") || goal_lower.contains("fix ") || goal_lower.contains("исправить") { + Some("plan") + } else if goal_lower.contains("создай") || goal_lower.contains("сгенерируй") || goal_lower.contains("create") || goal_lower.contains("с нуля") { + Some("apply") + } else { + None + }; + + let last_plan_ref = last_plan_json.as_deref(); + let last_ctx_ref = last_context.as_deref(); + return match llm_planner::plan( + &user_prefs_path, + &project_prefs_path, + &path, + &report_json, + goal_trim, + content_for_plan, + design_ref, + trends_ref, + output_format_override, + last_plan_ref, + last_ctx_ref, + ) + .await + { + Ok(plan) => plan, + Err(e) => AgentPlan { + ok: false, + summary: String::new(), + actions: vec![], + error: Some(e), + error_code: Some("LLM_ERROR".into()), + plan_json: None, + plan_context: None, + }, + }; + } + + // Запросы не про код/проект — не предлагать план с LICENSE, а ответить коротко. + let goal_trim = user_goal.trim(); + let goal_lower = goal_trim.to_lowercase(); + let off_topic = goal_lower.is_empty() + || goal_lower.contains("погода") + || goal_lower.contains("weather") + || goal_lower.contains("как дела") + || goal_lower.contains("what's the") + || goal_lower == "привет" + || goal_lower == "hello" + || goal_lower == "hi"; + if off_topic { + return AgentPlan { + ok: true, + summary: "Я помогаю с кодом и проектами. Напиши, например: «сделай README», «добавь тесты», «создай проект с нуля».".into(), + actions: vec![], + error: None, + error_code: None, + plan_json: None, + plan_context: None, + }; + } + + // При запросе «создать программу» сначала скелет (README, .gitignore, точка входа), LICENSE — в конце. + let want_skeleton = goal_lower.contains("создаю программу") + || goal_lower.contains("создать программу") + || goal_lower.contains("create a program") + || goal_lower.contains("create program") + || goal_lower.contains("новая программа") + || goal_lower.contains("с нуля") + || goal_lower.contains("from scratch"); + + let mut actions: Vec = vec![]; + let mut summary: Vec = vec![]; + + if !has_readme(&path) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: "README.md".into(), + content: Some(format!( + "# PAPA YU Project\n\n## Цель\n{}\n\n## Как запустить\n- (добавить)\n\n## Структура\n- (добавить)\n", + user_goal + )), + }); + summary.push("Добавлю README.md".into()); + } + + if !has_gitignore(&path) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: ".gitignore".into(), + content: Some( + "node_modules/\ndist/\nbuild/\n.DS_Store\n.env\n.env.*\ncoverage/\n.target/\n".into(), + ), + }); + summary.push("Добавлю .gitignore".into()); + } + + // При «создать программу»: добавить минимальную точку входа, если нет ни src/, ни main. + let has_src = root.join("src").is_dir(); + let has_main = root.join("main.py").exists() + || root.join("main.js").exists() + || root.join("src").join("main.py").exists() + || root.join("src").join("main.js").exists(); + if want_skeleton && !has_src && !has_main { + let main_path = "main.py"; + if !root.join(main_path).exists() { + actions.push(Action { + kind: ActionKind::CreateFile, + path: main_path.into(), + content: Some( + "\"\"\"Точка входа. Запуск: python main.py\"\"\"\n\ndef main() -> None:\n print(\"Hello\")\n\n\nif __name__ == \"__main__\":\n main()\n".into(), + ), + }); + summary.push("Добавлю main.py (скелет)".into()); + } + } + + if !has_license(&path) { + actions.push(Action { + kind: ActionKind::CreateFile, + path: "LICENSE".into(), + content: Some("UNLICENSED\n".into()), + }); + summary.push("Добавлю LICENSE (пометка UNLICENSED)".into()); + } + + if report_json.contains(".env") { + actions.push(Action { + kind: ActionKind::CreateFile, + path: ".env.example".into(), + content: Some("VITE_API_URL=\n# пример, без секретов\n".into()), + }); + summary.push("Добавлю .env.example (без секретов)".into()); + } + + if actions.is_empty() { + return AgentPlan { + ok: true, + summary: "Нет безопасных минимальных правок, которые можно применить автоматически.".into(), + actions, + error: None, + error_code: None, + plan_json: None, + plan_context: None, + }; + } + + AgentPlan { + ok: true, + summary: format!("План действий: {}", summary.join(", ")), + actions, + error: None, + error_code: None, + plan_json: None, + plan_context: None, + } +} diff --git a/src-tauri/src/commands/redo_last.rs b/src-tauri/src/commands/redo_last.rs new file mode 100644 index 0000000..d1891ca --- /dev/null +++ b/src-tauri/src/commands/redo_last.rs @@ -0,0 +1,57 @@ +use std::path::Path; +use tauri::AppHandle; + +use crate::tx::{apply_actions_to_disk, pop_redo, push_undo, read_manifest}; +use crate::types::RedoResult; + +#[tauri::command] +pub async fn redo_last(app: AppHandle) -> RedoResult { + let Some(tx_id) = pop_redo(&app) else { + return RedoResult { + ok: false, + tx_id: None, + error: Some("nothing to redo".into()), + error_code: Some("REDO_NOTHING".into()), + }; + }; + + let manifest = match read_manifest(&app, &tx_id) { + Ok(m) => m, + Err(e) => { + return RedoResult { + ok: false, + tx_id: Some(tx_id), + error: Some(e.to_string()), + error_code: Some("REDO_READ_MANIFEST_FAILED".into()), + }; + } + }; + + if manifest.applied_actions.is_empty() { + return RedoResult { + ok: false, + tx_id: Some(tx_id), + error: Some("Legacy transaction cannot be redone (no applied_actions)".into()), + error_code: Some("REDO_LEGACY".into()), + }; + } + + let root = Path::new(&manifest.root_path); + if let Err(e) = apply_actions_to_disk(root, &manifest.applied_actions) { + return RedoResult { + ok: false, + tx_id: Some(tx_id), + error: Some(e), + error_code: Some("REDO_APPLY_FAILED".into()), + }; + } + + let _ = push_undo(&app, tx_id.clone()); + + RedoResult { + ok: true, + tx_id: Some(tx_id), + error: None, + error_code: None, + } +} diff --git a/src-tauri/src/commands/run_batch.rs b/src-tauri/src/commands/run_batch.rs new file mode 100644 index 0000000..502a663 --- /dev/null +++ b/src-tauri/src/commands/run_batch.rs @@ -0,0 +1,87 @@ +use std::path::Path; + +use crate::commands::get_project_profile::get_project_limits; +use crate::commands::{analyze_project, apply_actions, preview_actions}; +use crate::tx::get_undo_redo_state; +use crate::types::{BatchEvent, BatchPayload}; +use tauri::AppHandle; + +pub async fn run_batch(app: AppHandle, payload: BatchPayload) -> Result, String> { + let mut events = Vec::new(); + + let paths = if payload.paths.is_empty() { + vec![".".to_string()] + } else { + payload.paths.clone() + }; + + let report = analyze_project(paths.clone(), payload.attached_files.clone()).map_err(|e| e.to_string())?; + events.push(BatchEvent { + kind: "report".to_string(), + report: Some(report.clone()), + preview: None, + apply_result: None, + message: Some(report.narrative.clone()), + undo_available: None, + }); + + let actions = payload + .selected_actions + .unwrap_or(report.actions.clone()); + if actions.is_empty() { + return Ok(events); + } + + let root_path = report.path.clone(); + let preview = preview_actions(crate::types::ApplyPayload { + root_path: root_path.clone(), + actions: actions.clone(), + auto_check: None, + label: None, + user_confirmed: false, + }) + .map_err(|e| e.to_string())?; + events.push(BatchEvent { + kind: "preview".to_string(), + report: None, + preview: Some(preview), + apply_result: None, + message: None, + undo_available: None, + }); + + if !payload.confirm_apply { + return Ok(events); + } + + let limits = get_project_limits(Path::new(&root_path)); + if actions.len() > limits.max_actions_per_tx as usize { + return Err(format!( + "too many actions: {} > {} (max_actions_per_tx)", + actions.len(), + limits.max_actions_per_tx + )); + } + + let result = apply_actions( + app.clone(), + crate::types::ApplyPayload { + root_path: root_path.clone(), + actions, + auto_check: Some(payload.auto_check), + label: None, + user_confirmed: payload.user_confirmed, + }, + ); + let (undo_avail, _) = get_undo_redo_state(&app); + events.push(BatchEvent { + kind: "apply".to_string(), + report: None, + preview: None, + apply_result: Some(result.clone()), + message: result.error.clone(), + undo_available: Some(result.ok && undo_avail), + }); + + Ok(events) +} diff --git a/src-tauri/src/commands/settings_export.rs b/src-tauri/src/commands/settings_export.rs new file mode 100644 index 0000000..57a6320 --- /dev/null +++ b/src-tauri/src/commands/settings_export.rs @@ -0,0 +1,223 @@ +//! v2.4.4: Export/import settings (projects, profiles, sessions, folder_links). + +use crate::commands::folder_links::{load_folder_links, save_folder_links, FolderLinks}; +use crate::store::{load_profiles, load_projects, load_sessions, save_profiles, save_projects, save_sessions}; +use crate::types::{Project, ProjectSettings, Session}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tauri::Manager; + +/// Bundle of all exportable settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SettingsBundle { + pub version: String, + pub exported_at: String, + pub projects: Vec, + pub profiles: HashMap, + pub sessions: Vec, + pub folder_links: FolderLinks, +} + +fn app_data_dir(app: &tauri::AppHandle) -> Result { + app.path().app_data_dir().map_err(|e| e.to_string()) +} + +/// Export all settings as JSON string +#[tauri::command] +pub fn export_settings(app: tauri::AppHandle) -> Result { + let dir = app_data_dir(&app)?; + + let bundle = SettingsBundle { + version: "2.4.4".to_string(), + exported_at: chrono::Utc::now().to_rfc3339(), + projects: load_projects(&dir), + profiles: load_profiles(&dir), + sessions: load_sessions(&dir), + folder_links: load_folder_links(&dir), + }; + + serde_json::to_string_pretty(&bundle).map_err(|e| e.to_string()) +} + +/// Import mode +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ImportMode { + /// Replace all existing settings + Replace, + /// Merge with existing (don't overwrite existing items) + Merge, +} + +/// Import settings from JSON string +#[tauri::command] +pub fn import_settings( + app: tauri::AppHandle, + json: String, + mode: Option, +) -> Result { + let bundle: SettingsBundle = serde_json::from_str(&json) + .map_err(|e| format!("Invalid settings JSON: {}", e))?; + + let mode = match mode.as_deref() { + Some("replace") => ImportMode::Replace, + _ => ImportMode::Merge, + }; + + let dir = app_data_dir(&app)?; + + let mut result = ImportResult { + projects_imported: 0, + profiles_imported: 0, + sessions_imported: 0, + folder_links_imported: 0, + }; + + match mode { + ImportMode::Replace => { + // Replace all + save_projects(&dir, &bundle.projects)?; + result.projects_imported = bundle.projects.len(); + + save_profiles(&dir, &bundle.profiles)?; + result.profiles_imported = bundle.profiles.len(); + + save_sessions(&dir, &bundle.sessions)?; + result.sessions_imported = bundle.sessions.len(); + + save_folder_links(&dir, &bundle.folder_links)?; + result.folder_links_imported = bundle.folder_links.paths.len(); + } + ImportMode::Merge => { + // Merge projects + let mut existing_projects = load_projects(&dir); + let existing_paths: std::collections::HashSet<_> = + existing_projects.iter().map(|p| p.path.clone()).collect(); + for p in bundle.projects { + if !existing_paths.contains(&p.path) { + existing_projects.push(p); + result.projects_imported += 1; + } + } + save_projects(&dir, &existing_projects)?; + + // Merge profiles + let mut existing_profiles = load_profiles(&dir); + for (k, v) in bundle.profiles { + if !existing_profiles.contains_key(&k) { + existing_profiles.insert(k, v); + result.profiles_imported += 1; + } + } + save_profiles(&dir, &existing_profiles)?; + + // Merge sessions + let mut existing_sessions = load_sessions(&dir); + let existing_ids: std::collections::HashSet<_> = + existing_sessions.iter().map(|s| s.id.clone()).collect(); + for s in bundle.sessions { + if !existing_ids.contains(&s.id) { + existing_sessions.push(s); + result.sessions_imported += 1; + } + } + save_sessions(&dir, &existing_sessions)?; + + // Merge folder links + let mut existing_links = load_folder_links(&dir); + let existing_set: std::collections::HashSet<_> = + existing_links.paths.iter().cloned().collect(); + for p in bundle.folder_links.paths { + if !existing_set.contains(&p) { + existing_links.paths.push(p); + result.folder_links_imported += 1; + } + } + save_folder_links(&dir, &existing_links)?; + } + } + + Ok(result) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportResult { + pub projects_imported: usize, + pub profiles_imported: usize, + pub sessions_imported: usize, + pub folder_links_imported: usize, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_bundle() -> SettingsBundle { + SettingsBundle { + version: "2.4.4".to_string(), + exported_at: "2025-01-31T00:00:00Z".to_string(), + projects: vec![Project { + id: "test-id".to_string(), + path: "/test/path".to_string(), + name: "Test Project".to_string(), + created_at: "2025-01-31T00:00:00Z".to_string(), + }], + profiles: HashMap::from([( + "test-id".to_string(), + ProjectSettings { + project_id: "test-id".to_string(), + auto_check: true, + max_attempts: 3, + max_actions: 10, + goal_template: Some("Test goal".to_string()), + }, + )]), + sessions: vec![], + folder_links: FolderLinks { + paths: vec!["/test/folder".to_string()], + }, + } + } + + #[test] + fn test_settings_bundle_serialization() { + let bundle = create_test_bundle(); + let json = serde_json::to_string(&bundle).unwrap(); + + assert!(json.contains("\"version\":\"2.4.4\"")); + assert!(json.contains("\"Test Project\"")); + assert!(json.contains("\"/test/folder\"")); + + let parsed: SettingsBundle = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.version, "2.4.4"); + assert_eq!(parsed.projects.len(), 1); + assert_eq!(parsed.projects[0].name, "Test Project"); + } + + #[test] + fn test_settings_bundle_deserialization() { + let json = r#"{ + "version": "2.4.4", + "exported_at": "2025-01-31T00:00:00Z", + "projects": [], + "profiles": {}, + "sessions": [], + "folder_links": { "paths": [] } + }"#; + + let bundle: SettingsBundle = serde_json::from_str(json).unwrap(); + assert_eq!(bundle.version, "2.4.4"); + assert!(bundle.projects.is_empty()); + } + + #[test] + fn test_import_result_default() { + let result = ImportResult { + projects_imported: 0, + profiles_imported: 0, + sessions_imported: 0, + folder_links_imported: 0, + }; + assert_eq!(result.projects_imported, 0); + } +} diff --git a/src-tauri/src/commands/trends.rs b/src-tauri/src/commands/trends.rs new file mode 100644 index 0000000..b877166 --- /dev/null +++ b/src-tauri/src/commands/trends.rs @@ -0,0 +1,184 @@ +//! Мониторинг трендов в программировании: рекомендации в автоматическом режиме не реже раз в месяц. +//! Данные хранятся в app_data_dir/trends.json; при первом запуске или если прошло >= 30 дней — should_update = true. + +use std::fs; +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use tauri::{AppHandle, Manager}; + +use crate::types::{TrendsRecommendation, TrendsResult}; + +const TRENDS_FILENAME: &str = "trends.json"; +const RECOMMEND_UPDATE_DAYS: i64 = 30; + +fn default_recommendations() -> Vec { + vec![ + TrendsRecommendation { + title: "TypeScript и строгая типизация".to_string(), + summary: Some("Использование TypeScript в веб- и Node-проектах снижает количество ошибок.".to_string()), + url: Some("https://www.typescriptlang.org/".to_string()), + source: Some("PAPA YU".to_string()), + }, + TrendsRecommendation { + title: "React Server Components и Next.js".to_string(), + summary: Some("Тренд на серверный рендеринг и стриминг в React-экосистеме.".to_string()), + url: Some("https://nextjs.org/".to_string()), + source: Some("PAPA YU".to_string()), + }, + TrendsRecommendation { + title: "Rust для инструментов и WASM".to_string(), + summary: Some("Rust растёт в CLI, инструментах и веб-сборке (WASM).".to_string()), + url: Some("https://www.rust-lang.org/".to_string()), + source: Some("PAPA YU".to_string()), + }, + TrendsRecommendation { + title: "Обновляйте зависимости и линтеры".to_string(), + summary: Some("Регулярно обновляйте npm/cargo зависимости и настройте линтеры (ESLint, Clippy).".to_string()), + url: None, + source: Some("PAPA YU".to_string()), + }, + ] +} + +fn app_trends_path(app: &AppHandle) -> Result { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + Ok(dir.join(TRENDS_FILENAME)) +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct StoredTrends { + last_updated: String, + recommendations: Vec, +} + +/// Возвращает сохранённые тренды и флаг should_update (true, если прошло >= 30 дней или данных нет). +#[tauri::command] +pub fn get_trends_recommendations(app: AppHandle) -> TrendsResult { + let path = match app_trends_path(&app) { + Ok(p) => p, + Err(_) => { + return TrendsResult { + last_updated: String::new(), + recommendations: default_recommendations(), + should_update: true, + }; + } + }; + if !path.exists() { + return TrendsResult { + last_updated: String::new(), + recommendations: default_recommendations(), + should_update: true, + }; + } + let content = match fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => { + return TrendsResult { + last_updated: String::new(), + recommendations: default_recommendations(), + should_update: true, + }; + } + }; + let stored: StoredTrends = match serde_json::from_str(&content) { + Ok(s) => s, + Err(_) => { + return TrendsResult { + last_updated: String::new(), + recommendations: default_recommendations(), + should_update: true, + }; + } + }; + let should_update = parse_and_check_older_than_days(&stored.last_updated, RECOMMEND_UPDATE_DAYS); + TrendsResult { + last_updated: stored.last_updated, + recommendations: stored.recommendations, + should_update, + } +} + +fn parse_and_check_older_than_days(iso: &str, days: i64) -> bool { + if iso.is_empty() { + return true; + } + let dt: DateTime = match DateTime::parse_from_rfc3339(iso) { + Ok(d) => d.with_timezone(&Utc), + Err(_) => return true, + }; + let now = Utc::now(); + (now - dt).num_days() >= days +} + +/// Разрешённые URL для запроса трендов (только эти домены). +const ALLOWED_TRENDS_HOSTS: &[&str] = &["raw.githubusercontent.com", "api.github.com", "jsonplaceholder.typicode.com"]; + +fn url_allowed(url: &str) -> bool { + let url = url.trim().to_lowercase(); + if !url.starts_with("https://") { + return false; + } + let rest = url.strip_prefix("https://").unwrap_or(""); + let host = rest.split('/').next().unwrap_or(""); + ALLOWED_TRENDS_HOSTS.iter().any(|h| host == *h || host.ends_with(&format!(".{}", h))) +} + +/// Обновляет тренды: запрашивает данные по allowlist URL (PAPAYU_TRENDS_URL или встроенный список) и сохраняет. +#[tauri::command] +pub async fn fetch_trends_recommendations(app: AppHandle) -> TrendsResult { + let now = Utc::now(); + let iso = now.to_rfc3339(); + + let urls: Vec = std::env::var("PAPAYU_TRENDS_URLS") + .ok() + .map(|s| s.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect()) + .unwrap_or_else(Vec::new); + + let mut recommendations = Vec::new(); + if !urls.is_empty() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(15)) + .build() + .unwrap_or_default(); + for url in urls { + if !url_allowed(&url) { + continue; + } + if let Ok(resp) = client.get(&url).send().await { + if let Ok(text) = resp.text().await { + if let Ok(parsed) = serde_json::from_str::>(&text) { + recommendations.extend(parsed); + } else if let Ok(obj) = serde_json::from_str::(&text) { + if let Some(arr) = obj.get("recommendations").and_then(|a| a.as_array()) { + for v in arr { + if let Ok(r) = serde_json::from_value::(v.clone()) { + recommendations.push(r); + } + } + } + } + } + } + } + } + if recommendations.is_empty() { + recommendations = default_recommendations(); + } + + let stored = StoredTrends { + last_updated: iso.clone(), + recommendations: recommendations.clone(), + }; + if let Ok(path) = app_trends_path(&app) { + let _ = fs::write(path, serde_json::to_string_pretty(&stored).unwrap_or_default()); + } + + TrendsResult { + last_updated: iso, + recommendations, + should_update: false, + } +} diff --git a/src-tauri/src/commands/undo_last.rs b/src-tauri/src/commands/undo_last.rs new file mode 100644 index 0000000..0ef51a7 --- /dev/null +++ b/src-tauri/src/commands/undo_last.rs @@ -0,0 +1,53 @@ +use tauri::AppHandle; + +use crate::tx::{get_undo_redo_state, pop_undo, push_redo, rollback_tx}; +use crate::types::{UndoAvailableResult, UndoRedoState, UndoResult}; + +#[tauri::command] +pub async fn get_undo_redo_state_cmd(app: AppHandle) -> UndoRedoState { + let (undo_available, redo_available) = get_undo_redo_state(&app); + UndoRedoState { + undo_available, + redo_available, + } +} + +#[tauri::command] +pub async fn undo_available(app: AppHandle) -> UndoAvailableResult { + let (undo_avail, _) = get_undo_redo_state(&app); + UndoAvailableResult { + ok: true, + available: undo_avail, + tx_id: None, + } +} + +#[tauri::command] +pub async fn undo_last(app: AppHandle) -> UndoResult { + let Some(tx_id) = pop_undo(&app) else { + return UndoResult { + ok: false, + tx_id: None, + error: Some("nothing to undo".into()), + error_code: Some("UNDO_NOTHING".into()), + }; + }; + + if let Err(e) = rollback_tx(&app, &tx_id) { + return UndoResult { + ok: false, + tx_id: Some(tx_id), + error: Some(e), + error_code: Some("ROLLBACK_FAILED".into()), + }; + } + + let _ = push_redo(&app, tx_id.clone()); + + UndoResult { + ok: true, + tx_id: Some(tx_id), + error: None, + error_code: None, + } +} diff --git a/src-tauri/src/commands/undo_last_tx.rs b/src-tauri/src/commands/undo_last_tx.rs new file mode 100644 index 0000000..e25d1eb --- /dev/null +++ b/src-tauri/src/commands/undo_last_tx.rs @@ -0,0 +1,97 @@ +//! v3.1: откат последней транзакции из history/tx + history/snapshots + +use std::fs; +use std::path::{Path, PathBuf}; + +use tauri::{AppHandle, Manager}; + +fn copy_dir(src: &Path, dst: &Path) -> Result<(), String> { + fs::create_dir_all(dst).map_err(|e| e.to_string())?; + for e in fs::read_dir(src).map_err(|e| e.to_string())? { + let e = e.map_err(|e| e.to_string())?; + let sp = e.path(); + let dp = dst.join(e.file_name()); + let ft = e.file_type().map_err(|e| e.to_string())?; + if ft.is_dir() { + copy_dir(&sp, &dp)?; + } else if ft.is_file() { + fs::copy(&sp, &dp).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +#[tauri::command] +pub async fn undo_last_tx(app: AppHandle, path: String) -> Result { + let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let tx_dir = data_dir.join("history").join("tx"); + let snap_base = data_dir.join("history").join("snapshots"); + + if !tx_dir.exists() { + return Ok(false); + } + + let mut items: Vec<(std::time::SystemTime, PathBuf)> = vec![]; + for e in fs::read_dir(&tx_dir).map_err(|e| e.to_string())? { + let e = e.map_err(|e| e.to_string())?; + let meta = e.metadata().map_err(|e| e.to_string())?; + let m = meta.modified().map_err(|e| e.to_string())?; + items.push((m, e.path())); + } + items.sort_by(|a, b| b.0.cmp(&a.0)); + let last = match items.first() { + Some((_, p)) => p.clone(), + None => return Ok(false), + }; + + let raw = fs::read_to_string(&last).map_err(|e| e.to_string())?; + let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?; + let tx_id = v + .get("txId") + .and_then(|x| x.as_str()) + .ok_or("txId missing")?; + let tx_path = v.get("path").and_then(|x| x.as_str()).unwrap_or(""); + if tx_path != path { + return Ok(false); + } + + let snap_dir = snap_base.join(tx_id); + if !snap_dir.exists() { + return Ok(false); + } + + let root = PathBuf::from(&path); + if !root.exists() { + return Ok(false); + } + + let exclude = [ + ".git", + "node_modules", + "dist", + "build", + ".next", + "target", + ".cache", + "coverage", + ]; + for entry in fs::read_dir(&root).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let p = entry.path(); + let name = entry.file_name(); + if exclude + .iter() + .any(|x| name.to_string_lossy().as_ref() == *x) + { + continue; + } + if p.is_dir() { + fs::remove_dir_all(&p).map_err(|e| e.to_string())?; + } else { + fs::remove_file(&p).map_err(|e| e.to_string())?; + } + } + + copy_dir(&snap_dir, &root)?; + Ok(true) +} diff --git a/src-tauri/src/commands/undo_status.rs b/src-tauri/src/commands/undo_status.rs new file mode 100644 index 0000000..8df4633 --- /dev/null +++ b/src-tauri/src/commands/undo_status.rs @@ -0,0 +1,36 @@ +//! v2.9.3: доступен ли откат (есть ли последняя транзакция в papayu/transactions) + +use std::fs; +use std::path::PathBuf; + +use tauri::{AppHandle, Manager}; + +use crate::types::UndoStatus; + +#[tauri::command] +pub async fn undo_status(app: AppHandle) -> UndoStatus { + let base: PathBuf = match app.path().app_data_dir() { + Ok(v) => v, + Err(_) => return UndoStatus { available: false, tx_id: None }, + }; + + let dir = base.join("history").join("tx"); + let Ok(rd) = fs::read_dir(&dir) else { + return UndoStatus { available: false, tx_id: None }; + }; + + let last = rd + .filter_map(|e| e.ok()) + .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok())); + + match last { + Some(f) => { + let name = f.file_name().to_string_lossy().to_string(); + UndoStatus { + available: true, + tx_id: Some(name), + } + } + None => UndoStatus { available: false, tx_id: None }, + } +} diff --git a/src-tauri/src/context.rs b/src-tauri/src/context.rs new file mode 100644 index 0000000..39e6bd5 --- /dev/null +++ b/src-tauri/src/context.rs @@ -0,0 +1,554 @@ +//! Автосбор контекста для LLM: env, project prefs, context_requests (read_file, search, logs). +//! Кеш read/search/logs/env в пределах сессии (plan-цикла). + +use crate::memory::EngineeringMemory; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +const MAX_CONTEXT_LINE_LEN: usize = 80_000; +const SEARCH_MAX_HITS: usize = 50; + +fn context_max_files() -> usize { + std::env::var("PAPAYU_CONTEXT_MAX_FILES") + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(8) +} + +fn context_max_file_chars() -> usize { + std::env::var("PAPAYU_CONTEXT_MAX_FILE_CHARS") + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(20_000) +} + +fn context_max_total_chars() -> usize { + std::env::var("PAPAYU_CONTEXT_MAX_TOTAL_CHARS") + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(120_000) +} + +#[allow(dead_code)] +fn context_max_log_chars() -> usize { + std::env::var("PAPAYU_CONTEXT_MAX_LOG_CHARS") + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(12_000) +} + +/// Ключ кеша контекста. +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +pub enum ContextCacheKey { + Env, + Logs { source: String, last_n: u32 }, + ReadFile { path: String, start: u32, end: u32 }, + Search { query: String, glob: Option }, +} + +/// Кеш контекста для сессии (plan-цикла). +#[derive(Default)] +pub struct ContextCache { + map: HashMap, +} + +impl ContextCache { + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } + + pub fn get(&self, key: &ContextCacheKey) -> Option<&String> { + self.map.get(key) + } + + pub fn put(&mut self, key: ContextCacheKey, value: String) { + self.map.insert(key, value); + } +} + +/// Собирает базовый контекст перед первым запросом к модели: env, команды из project prefs. +pub fn gather_base_context(_project_root: &Path, mem: &EngineeringMemory) -> String { + let mut parts = Vec::new(); + + let env_block = gather_env(); + if !env_block.is_empty() { + parts.push(format!("ENV:\n{}", env_block)); + } + + if !mem.project.is_default() { + let mut prefs = Vec::new(); + if !mem.project.default_test_command.is_empty() { + prefs.push(format!("default_test_command: {}", mem.project.default_test_command)); + } + if !mem.project.default_lint_command.is_empty() { + prefs.push(format!("default_lint_command: {}", mem.project.default_lint_command)); + } + if !mem.project.default_format_command.is_empty() { + prefs.push(format!("default_format_command: {}", mem.project.default_format_command)); + } + if !mem.project.src_roots.is_empty() { + prefs.push(format!("src_roots: {:?}", mem.project.src_roots)); + } + if !mem.project.test_roots.is_empty() { + prefs.push(format!("test_roots: {:?}", mem.project.test_roots)); + } + if !prefs.is_empty() { + parts.push(format!("PROJECT_PREFS:\n{}", prefs.join("\n"))); + } + } + + if parts.is_empty() { + String::new() + } else { + format!("\n\nAUTO_CONTEXT:\n{}\n", parts.join("\n\n")) + } +} + +fn gather_env() -> String { + let mut lines = Vec::new(); + if let Ok(os) = std::env::var("OS") { + lines.push(format!("OS: {}", os)); + } + #[cfg(target_os = "macos")] + lines.push("OS: macOS".to_string()); + #[cfg(target_os = "linux")] + lines.push("OS: Linux".to_string()); + #[cfg(target_os = "windows")] + lines.push("OS: Windows".to_string()); + if let Ok(lang) = std::env::var("LANG") { + lines.push(format!("LANG: {}", lang)); + } + if let Ok(py) = std::env::var("VIRTUAL_ENV") { + lines.push(format!("VIRTUAL_ENV: {}", py)); + } + if let Ok(node) = std::env::var("NODE_VERSION") { + lines.push(format!("NODE_VERSION: {}", node)); + } + lines.join("\n") +} + +/// Выполняет context_requests от модели и возвращает текст для добавления в user message. +/// Использует кеш, если передан; логирует CONTEXT_CACHE_HIT/MISS при trace_id. +pub fn fulfill_context_requests( + project_root: &Path, + requests: &[serde_json::Value], + max_log_lines: usize, + mut cache: Option<&mut ContextCache>, + trace_id: Option<&str>, +) -> String { + let mut parts = Vec::new(); + for r in requests { + let obj = match r.as_object() { + Some(o) => o, + None => continue, + }; + let rtype = obj.get("type").and_then(|v| v.as_str()).unwrap_or(""); + match rtype { + "read_file" => { + if let Some(path) = obj.get("path").and_then(|v| v.as_str()) { + let start = obj.get("start_line").and_then(|v| v.as_u64()).unwrap_or(1) as u32; + let end = obj + .get("end_line") + .and_then(|v| v.as_u64()) + .unwrap_or((start + 200) as u64) as u32; + let key = ContextCacheKey::ReadFile { + path: path.to_string(), + start, + end, + }; + let content = if let Some(ref mut c) = cache { + if let Some(v) = c.get(&key) { + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_HIT key=read_file path={}", tid, path); + } + v.clone() + } else { + let v = read_file_snippet(project_root, path, start as usize, end as usize); + let out = format!("FILE[{}]:\n{}", path, v); + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_MISS key=read_file path={} size={}", tid, path, out.len()); + } + c.put(key, out.clone()); + out + } + } else { + let v = read_file_snippet(project_root, path, start as usize, end as usize); + format!("FILE[{}]:\n{}", path, v) + }; + parts.push(content); + } + } + "search" => { + if let Some(query) = obj.get("query").and_then(|v| v.as_str()) { + let glob = obj.get("glob").and_then(|v| v.as_str()).map(String::from); + let key = ContextCacheKey::Search { + query: query.to_string(), + glob: glob.clone(), + }; + let content = if let Some(ref mut c) = cache { + if let Some(v) = c.get(&key) { + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_HIT key=search query={}", tid, query); + } + v.clone() + } else { + let hits = search_in_project(project_root, query, glob.as_deref()); + let out = format!("SEARCH[{}]:\n{}", query, hits.join("\n")); + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_MISS key=search query={} hits={}", tid, query, hits.len()); + } + c.put(key, out.clone()); + out + } + } else { + let hits = search_in_project(project_root, query, glob.as_deref()); + format!("SEARCH[{}]:\n{}", query, hits.join("\n")) + }; + parts.push(content); + } + } + "logs" => { + let source = obj.get("source").and_then(|v| v.as_str()).unwrap_or("runtime"); + let last_n = obj + .get("last_n") + .and_then(|v| v.as_u64()) + .unwrap_or(max_log_lines as u64) as u32; + let key = ContextCacheKey::Logs { + source: source.to_string(), + last_n, + }; + let content = if let Some(ref mut c) = cache { + if let Some(v) = c.get(&key) { + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_HIT key=logs source={}", tid, source); + } + v.clone() + } else { + let v = format!( + "LOGS[{}]: (last_n={}; приложение не имеет доступа к логам runtime — передай вывод в запросе)\n", + source, last_n + ); + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_MISS key=logs source={}", tid, source); + } + c.put(key, v.clone()); + v + } + } else { + format!( + "LOGS[{}]: (last_n={}; приложение не имеет доступа к логам runtime — передай вывод в запросе)\n", + source, last_n + ) + }; + parts.push(content); + } + "env" => { + let key = ContextCacheKey::Env; + let content = if let Some(ref mut c) = cache { + if let Some(v) = c.get(&key) { + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_HIT key=env", tid); + } + v.clone() + } else { + let v = format!("ENV (повторно):\n{}", gather_env()); + if let Some(tid) = trace_id { + eprintln!("[{}] CONTEXT_CACHE_MISS key=env size={}", tid, v.len()); + } + c.put(key, v.clone()); + v + } + } else { + format!("ENV (повторно):\n{}", gather_env()) + }; + parts.push(content); + } + _ => {} + } + } + if parts.is_empty() { + String::new() + } else { + let max_files = context_max_files(); + let max_total = context_max_total_chars(); + let header = "\n\nFULFILLED_CONTEXT:\n"; + let mut total_chars = header.len(); + let mut result_parts = Vec::with_capacity(parts.len().min(max_files)); + let mut dropped = 0; + for (_i, p) in parts.iter().enumerate() { + if result_parts.len() >= max_files { + dropped += 1; + continue; + } + let part_len = p.len() + if result_parts.is_empty() { 0 } else { 2 }; + if total_chars + part_len > max_total && !result_parts.is_empty() { + dropped += 1; + continue; + } + let to_add = if total_chars + part_len > max_total { + let allowed = max_total - total_chars - 30; + if allowed > 100 { + format!("{}...[TRUNCATED]...", &p[..allowed.min(p.len())]) + } else { + p.clone() + } + } else { + p.clone() + }; + total_chars += to_add.len() + if result_parts.is_empty() { 0 } else { 2 }; + result_parts.push(to_add); + } + if let Some(tid) = trace_id { + if dropped > 0 { + eprintln!( + "[{}] CONTEXT_DIET_APPLIED files={} dropped={} total_chars={}", + tid, result_parts.len(), dropped, total_chars + ); + } + } + format!("{}{}", header, result_parts.join("\n\n")) + } +} + +fn read_file_snippet(root: &Path, rel_path: &str, start_line: usize, end_line: usize) -> String { + let path = root.join(rel_path); + if !path.is_file() { + return format!("(файл не найден: {})", rel_path); + } + let content = match fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => return "(не удалось прочитать)".to_string(), + }; + let lines: Vec<&str> = content.lines().collect(); + let start = start_line.saturating_sub(1).min(lines.len()); + let end = end_line.min(lines.len()).max(start); + let slice: Vec<&str> = lines.get(start..end).unwrap_or(&[]).into_iter().copied().collect(); + let mut out = String::new(); + for (i, line) in slice.iter().enumerate() { + let line_no = start + i + 1; + out.push_str(&format!("{}|{}\n", line_no, line)); + } + let max_chars = context_max_file_chars().min(MAX_CONTEXT_LINE_LEN); + if out.len() > max_chars { + let head = (max_chars as f32 * 0.6) as usize; + let tail = max_chars - head - 30; + format!( + "{}...[TRUNCATED {} chars]...\n{}", + &out[..head.min(out.len())], + out.len(), + &out[out.len().saturating_sub(tail)..] + ) + } else { + out + } +} + +fn search_in_project(root: &Path, query: &str, _glob: Option<&str>) -> Vec { + let mut hits = Vec::new(); + let walk = walkdir::WalkDir::new(root) + .follow_links(false) + .into_iter() + .filter_entry(|e| { + let n = e.file_name().to_str().unwrap_or(""); + !n.starts_with('.') + && n != "node_modules" + && n != "target" + && n != "dist" + && n != "__pycache__" + }); + for entry in walk.filter_map(|e| e.ok()) { + let path = entry.path(); + if !path.is_file() { + continue; + } + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + let is_text = ["py", "rs", "ts", "tsx", "js", "jsx", "md", "json", "toml", "yml", "yaml"] + .contains(&ext); + if !is_text { + continue; + } + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => continue, + }; + for (i, line) in content.lines().enumerate() { + if line.contains(query) { + let rel = path + .strip_prefix(root) + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| path.display().to_string()); + hits.push(format!("{}:{}: {}", rel, i + 1, line.trim())); + if hits.len() >= SEARCH_MAX_HITS { + return hits; + } + } + } + } + hits +} + +/// Эвристики автосбора контекста до первого вызова LLM. +/// Возвращает дополнительный контекст на основе user_goal/report (Traceback, ImportError и т.д.). +pub fn gather_auto_context_from_message(project_root: &Path, user_message: &str) -> String { + let mut parts = Vec::new(); + + // Traceback / Exception → извлечь пути и прочитать файлы ±80 строк + let traceback_files = extract_traceback_files(user_message); + let root_str = project_root.display().to_string(); + for (path_from_tb, line_no) in traceback_files { + // Преобразовать абсолютный путь в относительный (если project_root — префикс) + let rel_path = if path_from_tb.starts_with('/') + || (path_from_tb.len() >= 2 && path_from_tb.chars().nth(1) == Some(':')) + { + // Абсолютный путь: убрать префикс project_root + let normalized = path_from_tb.replace('\\', "/"); + let root_norm = root_str.replace('\\', "/"); + if normalized.starts_with(&root_norm) { + normalized + .strip_prefix(&root_norm) + .map(|s| s.trim_start_matches('/').to_string()) + .unwrap_or(path_from_tb) + } else { + path_from_tb + } + } else { + path_from_tb + }; + let start = line_no.saturating_sub(80).max(1); + let end = line_no + 80; + let content = read_file_snippet(project_root, &rel_path, start, end); + if !content.contains("не найден") && !content.contains("не удалось") { + parts.push(format!("AUTO_TRACEBACK[{}]:\n{}", rel_path, content)); + } + } + + // ImportError / ModuleNotFoundError → env + lock/deps файлы + let lower = user_message.to_lowercase(); + if lower.contains("importerror") + || lower.contains("modulenotfounderror") + || lower.contains("cannot find module") + || lower.contains("module not found") + { + parts.push(format!("ENV (для ImportError):\n{}", gather_env())); + // Попытаться добавить содержимое pyproject.toml, requirements.txt, package.json + for rel in ["pyproject.toml", "requirements.txt", "package.json", "poetry.lock"] { + let p = project_root.join(rel); + if p.is_file() { + if let Ok(s) = fs::read_to_string(&p) { + let trimmed = if s.len() > 8000 { + format!("{}…\n(обрезано)", &s[..8000]) + } else { + s + }; + parts.push(format!("DEPS[{}]:\n{}", rel, trimmed)); + } + } + } + } + + if parts.is_empty() { + String::new() + } else { + format!("\n\nAUTO_CONTEXT_FROM_MESSAGE:\n{}\n", parts.join("\n\n")) + } +} + +/// Извлекает пути и строки из traceback в тексте (Python). Используется при автосборе контекста по ошибке. +pub fn extract_traceback_files(text: &str) -> Vec<(String, usize)> { + let mut out = Vec::new(); + for line in text.lines() { + let line = line.trim(); + if line.starts_with("File \"") { + if let Some(rest) = line.strip_prefix("File \"") { + if let Some(end) = rest.find('\"') { + let path = rest[..end].to_string(); + let after = &rest[end + 1..]; + let line_no = after + .trim_start_matches(", line ") + .split(',') + .next() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + if !path.is_empty() && line_no > 0 { + out.push((path, line_no)); + } + } + } + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cache_read_file_hit() { + let mut cache = ContextCache::new(); + let key = ContextCacheKey::ReadFile { + path: "foo.rs".to_string(), + start: 1, + end: 10, + }; + cache.put(key.clone(), "FILE[foo.rs]:\n1|line1".to_string()); + assert!(cache.get(&key).is_some()); + assert!(cache.get(&key).unwrap().contains("foo.rs")); + } + + #[test] + fn test_cache_search_hit() { + let mut cache = ContextCache::new(); + let key = ContextCacheKey::Search { + query: "test".to_string(), + glob: None, + }; + cache.put(key.clone(), "SEARCH[test]:\nfoo:1: test".to_string()); + assert!(cache.get(&key).is_some()); + } + + #[test] + fn test_cache_env_hit() { + let mut cache = ContextCache::new(); + let key = ContextCacheKey::Env; + cache.put(key.clone(), "ENV:\nOS: test".to_string()); + assert!(cache.get(&key).is_some()); + } + + #[test] + fn test_cache_logs_hit() { + let mut cache = ContextCache::new(); + let key = ContextCacheKey::Logs { + source: "runtime".to_string(), + last_n: 100, + }; + cache.put(key.clone(), "LOGS[runtime]: ...".to_string()); + assert!(cache.get(&key).is_some()); + } + + #[test] + fn test_context_diet_max_files() { + let max = context_max_files(); + assert!(max >= 1 && max <= 100); + } + + #[test] + fn test_context_diet_limits() { + assert!(context_max_file_chars() > 1000); + assert!(context_max_total_chars() > 10000); + } + + #[test] + fn extract_traceback_parses_file_line() { + let t = r#" File "/home/x/src/main.py", line 42, in foo + bar() +"#; + let files = extract_traceback_files(t); + assert_eq!(files.len(), 1); + assert!(files[0].0.contains("main.py")); + assert_eq!(files[0].1, 42); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..a28ab26 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,90 @@ +mod commands; +mod context; +mod memory; +mod store; +mod tx; +mod types; +mod verify; + +use commands::{add_project, agentic_run, analyze_project, append_session_event, apply_actions, apply_actions_tx, export_settings, fetch_trends_recommendations, generate_actions, generate_actions_from_report, get_project_profile, get_project_settings, get_trends_recommendations, get_undo_redo_state_cmd, import_settings, list_projects, list_sessions, load_folder_links, preview_actions, propose_actions, redo_last, run_batch, save_folder_links, set_project_settings, undo_available, undo_last, undo_last_tx, undo_status}; +use tauri::Manager; +use commands::FolderLinks; +use types::{ApplyPayload, BatchPayload}; + +#[tauri::command] +fn analyze_project_cmd(paths: Vec, attached_files: Option>) -> Result { + analyze_project(paths, attached_files) +} + +#[tauri::command] +fn preview_actions_cmd(payload: ApplyPayload) -> Result { + preview_actions(payload) +} + +#[tauri::command] +fn apply_actions_cmd(app: tauri::AppHandle, payload: ApplyPayload) -> types::ApplyResult { + apply_actions(app, payload) +} + +#[tauri::command] +async fn run_batch_cmd(app: tauri::AppHandle, payload: BatchPayload) -> Result, String> { + run_batch(app, payload).await +} + +#[tauri::command] +fn get_folder_links(app: tauri::AppHandle) -> Result { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + Ok(load_folder_links(&dir)) +} + +#[tauri::command] +fn set_folder_links(app: tauri::AppHandle, links: FolderLinks) -> Result<(), String> { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + save_folder_links(&dir, &links) +} + +/// Проверка целостности проекта (типы, сборка, тесты). Вызывается автоматически после применений или вручную. +#[tauri::command] +fn verify_project(path: String) -> types::VerifyResult { + verify::verify_project(&path) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + analyze_project_cmd, + preview_actions_cmd, + apply_actions_cmd, + undo_last, + undo_available, + redo_last, + get_undo_redo_state_cmd, + generate_actions, + run_batch_cmd, + get_folder_links, + set_folder_links, + apply_actions_tx, + verify_project, + undo_last_tx, + undo_status, + propose_actions, + generate_actions_from_report, + agentic_run, + list_projects, + add_project, + get_project_profile, + get_project_settings, + set_project_settings, + list_sessions, + append_session_event, + get_trends_recommendations, + fetch_trends_recommendations, + export_settings, + import_settings, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..d8b0710 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + papa_yu_lib::run() +} diff --git a/src-tauri/src/memory.rs b/src-tauri/src/memory.rs new file mode 100644 index 0000000..9afee77 --- /dev/null +++ b/src-tauri/src/memory.rs @@ -0,0 +1,301 @@ +//! Инженерная память: user prefs + project prefs, загрузка/сохранение, MEMORY BLOCK для промпта, whitelist для memory_patch. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +pub const SCHEMA_VERSION: u32 = 1; + +/// User preferences (оператор). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct UserPrefs { + #[serde(default)] + pub preferred_style: String, // "brief" | "normal" | "verbose" + #[serde(default)] + pub ask_budget: u8, // 0..2 + #[serde(default)] + pub risk_tolerance: String, // "low" | "medium" | "high" + #[serde(default)] + pub default_language: String, // "python" | "node" | "go" etc. + #[serde(default)] + pub output_format: String, // "patch_first" | "plan_first" +} + +/// Project preferences (для конкретного репо). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProjectPrefs { + #[serde(default)] + pub default_test_command: String, + #[serde(default)] + pub default_lint_command: String, + #[serde(default)] + pub default_format_command: String, + #[serde(default)] + pub package_manager: String, + #[serde(default)] + pub build_command: String, + #[serde(default)] + pub src_roots: Vec, + #[serde(default)] + pub test_roots: Vec, + #[serde(default)] + pub ci_notes: String, +} + +/// Корневой файл пользовательских настроек (~/.papa-yu или app_data/papa-yu). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PreferencesFile { + #[serde(default)] + pub schema_version: u32, + #[serde(default)] + pub user: UserPrefs, +} + +/// Файл настроек проекта (.papa-yu/project.json). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectPrefsFile { + #[serde(default)] + pub schema_version: u32, + #[serde(default)] + pub project: ProjectPrefs, +} + +/// Объединённый вид памяти для промпта (только непустые поля). +#[derive(Debug, Clone, Default)] +pub struct EngineeringMemory { + pub user: UserPrefs, + pub project: ProjectPrefs, +} + +impl UserPrefs { + pub(crate) fn is_default(&self) -> bool { + self.preferred_style.is_empty() + && self.ask_budget == 0 + && self.risk_tolerance.is_empty() + && self.default_language.is_empty() + && self.output_format.is_empty() + } +} + +impl ProjectPrefs { + pub(crate) fn is_default(&self) -> bool { + self.default_test_command.is_empty() + && self.default_lint_command.is_empty() + && self.default_format_command.is_empty() + && self.package_manager.is_empty() + && self.build_command.is_empty() + && self.src_roots.is_empty() + && self.test_roots.is_empty() + && self.ci_notes.is_empty() + } +} + +/// Разрешённые ключи для memory_patch (dot-notation: user.*, project.*). +const MEMORY_PATCH_WHITELIST: &[&str] = &[ + "user.preferred_style", + "user.ask_budget", + "user.risk_tolerance", + "user.default_language", + "user.output_format", + "project.default_test_command", + "project.default_lint_command", + "project.default_format_command", + "project.package_manager", + "project.build_command", + "project.src_roots", + "project.test_roots", + "project.ci_notes", +]; + +fn is_whitelisted(key: &str) -> bool { + MEMORY_PATCH_WHITELIST.contains(&key) +} + +/// Загружает user prefs из файла (создаёт дефолт, если файла нет). +pub fn load_user_prefs(path: &Path) -> UserPrefs { + let s = match fs::read_to_string(path) { + Ok(s) => s, + Err(_) => return UserPrefs::default(), + }; + let file: PreferencesFile = match serde_json::from_str(&s) { + Ok(f) => f, + Err(_) => return UserPrefs::default(), + }; + file.user +} + +/// Загружает project prefs из .papa-yu/project.json (дефолт, если нет файла). +pub fn load_project_prefs(path: &Path) -> ProjectPrefs { + let s = match fs::read_to_string(path) { + Ok(s) => s, + Err(_) => return ProjectPrefs::default(), + }; + let file: ProjectPrefsFile = match serde_json::from_str(&s) { + Ok(f) => f, + Err(_) => return ProjectPrefs::default(), + }; + file.project +} + +/// Собирает объединённую память: user из user_prefs_path, project из project_prefs_path. +pub fn load_memory(user_prefs_path: &Path, project_prefs_path: &Path) -> EngineeringMemory { + let user = load_user_prefs(user_prefs_path); + let project = load_project_prefs(project_prefs_path); + EngineeringMemory { user, project } +} + +/// Формирует текст MEMORY BLOCK для вставки в system prompt (~1–2 KB). +pub fn build_memory_block(mem: &EngineeringMemory) -> String { + if mem.user.is_default() && mem.project.is_default() { + return String::new(); + } + let mut obj = serde_json::Map::new(); + if !mem.user.is_default() { + let mut user = serde_json::Map::new(); + if !mem.user.preferred_style.is_empty() { + user.insert("preferred_style".into(), serde_json::Value::String(mem.user.preferred_style.clone())); + } + if mem.user.ask_budget > 0 { + user.insert("ask_budget".into(), serde_json::Value::Number(serde_json::Number::from(mem.user.ask_budget))); + } + if !mem.user.risk_tolerance.is_empty() { + user.insert("risk_tolerance".into(), serde_json::Value::String(mem.user.risk_tolerance.clone())); + } + if !mem.user.default_language.is_empty() { + user.insert("default_language".into(), serde_json::Value::String(mem.user.default_language.clone())); + } + if !mem.user.output_format.is_empty() { + user.insert("output_format".into(), serde_json::Value::String(mem.user.output_format.clone())); + } + obj.insert("user".into(), serde_json::Value::Object(user)); + } + if !mem.project.is_default() { + let mut project = serde_json::Map::new(); + if !mem.project.default_test_command.is_empty() { + project.insert("default_test_command".into(), serde_json::Value::String(mem.project.default_test_command.clone())); + } + if !mem.project.default_lint_command.is_empty() { + project.insert("default_lint_command".into(), serde_json::Value::String(mem.project.default_lint_command.clone())); + } + if !mem.project.default_format_command.is_empty() { + project.insert("default_format_command".into(), serde_json::Value::String(mem.project.default_format_command.clone())); + } + if !mem.project.package_manager.is_empty() { + project.insert("package_manager".into(), serde_json::Value::String(mem.project.package_manager.clone())); + } + if !mem.project.build_command.is_empty() { + project.insert("build_command".into(), serde_json::Value::String(mem.project.build_command.clone())); + } + if !mem.project.src_roots.is_empty() { + project.insert("src_roots".into(), serde_json::to_value(&mem.project.src_roots).unwrap_or(serde_json::Value::Array(vec![]))); + } + if !mem.project.test_roots.is_empty() { + project.insert("test_roots".into(), serde_json::to_value(&mem.project.test_roots).unwrap_or(serde_json::Value::Array(vec![]))); + } + if !mem.project.ci_notes.is_empty() { + project.insert("ci_notes".into(), serde_json::Value::String(mem.project.ci_notes.clone())); + } + obj.insert("project".into(), serde_json::Value::Object(project)); + } + if obj.is_empty() { + return String::new(); + } + let json_str = serde_json::to_string(&serde_json::Value::Object(obj)).unwrap_or_default(); + format!( + "\n\nENGINEERING_MEMORY (trusted by user; update only when user requests):\n{}\n\nUse ENGINEERING_MEMORY as defaults. If user explicitly asks to change — suggest updating memory and show new JSON.", + json_str + ) +} + +/// Применяет memory_patch (ключи через точку, только whitelist). Возвращает обновлённые user + project. +pub fn apply_memory_patch( + patch: &HashMap, + current_user: &UserPrefs, + current_project: &ProjectPrefs, +) -> (UserPrefs, ProjectPrefs) { + let mut user = current_user.clone(); + let mut project = current_project.clone(); + for (key, value) in patch { + if !is_whitelisted(key) { + continue; + } + if key.starts_with("user.") { + let field = &key[5..]; + match field { + "preferred_style" => if let Some(s) = value.as_str() { user.preferred_style = s.to_string(); }, + "ask_budget" => if let Some(n) = value.as_u64() { user.ask_budget = n as u8; }, + "risk_tolerance" => if let Some(s) = value.as_str() { user.risk_tolerance = s.to_string(); }, + "default_language" => if let Some(s) = value.as_str() { user.default_language = s.to_string(); }, + "output_format" => if let Some(s) = value.as_str() { user.output_format = s.to_string(); }, + _ => {} + } + } else if key.starts_with("project.") { + let field = &key[8..]; + match field { + "default_test_command" => if let Some(s) = value.as_str() { project.default_test_command = s.to_string(); }, + "default_lint_command" => if let Some(s) = value.as_str() { project.default_lint_command = s.to_string(); }, + "default_format_command" => if let Some(s) = value.as_str() { project.default_format_command = s.to_string(); }, + "package_manager" => if let Some(s) = value.as_str() { project.package_manager = s.to_string(); }, + "build_command" => if let Some(s) = value.as_str() { project.build_command = s.to_string(); }, + "src_roots" => if let Some(arr) = value.as_array() { + project.src_roots = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect(); + }, + "test_roots" => if let Some(arr) = value.as_array() { + project.test_roots = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect(); + }, + "ci_notes" => if let Some(s) = value.as_str() { project.ci_notes = s.to_string(); }, + _ => {} + } + } + } + (user, project) +} + +/// Сохраняет user prefs в файл. Создаёт родительскую папку при необходимости. +pub fn save_user_prefs(path: &Path, user: &UserPrefs) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let file = PreferencesFile { + schema_version: SCHEMA_VERSION, + user: user.clone(), + }; + let s = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?; + fs::write(path, s).map_err(|e| e.to_string()) +} + +/// Сохраняет project prefs в .papa-yu/project.json. +pub fn save_project_prefs(path: &Path, project: &ProjectPrefs) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let file = ProjectPrefsFile { + schema_version: SCHEMA_VERSION, + project: project.clone(), + }; + let s = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?; + fs::write(path, s).map_err(|e| e.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn whitelist_accepts_user_and_project() { + assert!(is_whitelisted("user.preferred_style")); + assert!(is_whitelisted("project.default_test_command")); + assert!(!is_whitelisted("session.foo")); + } + + #[test] + fn apply_patch_updates_user_and_project() { + let mut patch = HashMap::new(); + patch.insert("user.preferred_style".into(), serde_json::Value::String("brief".into())); + patch.insert("project.default_test_command".into(), serde_json::Value::String("pytest -q".into())); + let (user, project) = apply_memory_patch(&patch, &UserPrefs::default(), &ProjectPrefs::default()); + assert_eq!(user.preferred_style, "brief"); + assert_eq!(project.default_test_command, "pytest -q"); + } +} diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs new file mode 100644 index 0000000..1ef53d4 --- /dev/null +++ b/src-tauri/src/store.rs @@ -0,0 +1,120 @@ +//! v2.5: Projects & sessions store (JSON in app_data_dir). + +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +use crate::types::{Project, ProjectSettings, Session, SessionEvent}; + +const PROJECTS_FILE: &str = "projects.json"; +const PROFILES_FILE: &str = "project_profiles.json"; +const SESSIONS_FILE: &str = "sessions.json"; + +const MAX_SESSIONS_PER_PROJECT: usize = 50; +const MAX_EVENTS_PER_SESSION: usize = 200; + +pub fn load_projects(app_data_dir: &Path) -> Vec { + let p = app_data_dir.join(PROJECTS_FILE); + if let Ok(s) = fs::read_to_string(&p) { + if let Ok(v) = serde_json::from_str::>(&s) { + return v; + } + } + vec![] +} + +pub fn save_projects(app_data_dir: &Path, projects: &[Project]) -> Result<(), String> { + fs::create_dir_all(app_data_dir).map_err(|e| e.to_string())?; + let p = app_data_dir.join(PROJECTS_FILE); + fs::write( + &p, + serde_json::to_string_pretty(projects).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string()) +} + +pub fn load_profiles(app_data_dir: &Path) -> HashMap { + let p = app_data_dir.join(PROFILES_FILE); + if let Ok(s) = fs::read_to_string(&p) { + if let Ok(m) = serde_json::from_str::>(&s) { + return m; + } + } + HashMap::new() +} + +pub fn save_profiles( + app_data_dir: &Path, + profiles: &HashMap, +) -> Result<(), String> { + fs::create_dir_all(app_data_dir).map_err(|e| e.to_string())?; + let p = app_data_dir.join(PROFILES_FILE); + fs::write( + &p, + serde_json::to_string_pretty(profiles).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string()) +} + +pub fn load_sessions(app_data_dir: &Path) -> Vec { + let p = app_data_dir.join(SESSIONS_FILE); + if let Ok(s) = fs::read_to_string(&p) { + if let Ok(v) = serde_json::from_str::>(&s) { + return v; + } + } + vec![] +} + +pub fn save_sessions(app_data_dir: &Path, sessions: &[Session]) -> Result<(), String> { + fs::create_dir_all(app_data_dir).map_err(|e| e.to_string())?; + let p = app_data_dir.join(SESSIONS_FILE); + fs::write( + &p, + serde_json::to_string_pretty(sessions).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string()) +} + +pub fn add_session_event( + app_data_dir: &Path, + project_id: &str, + event: SessionEvent, +) -> Result { + let mut sessions = load_sessions(app_data_dir); + let now = chrono::Utc::now().to_rfc3339(); + let idx = sessions + .iter() + .enumerate() + .filter(|(_, s)| s.project_id == project_id) + .max_by_key(|(_, s)| s.updated_at.as_str()) + .map(|(i, _)| i); + + if let Some(i) = idx { + let s = &mut sessions[i]; + s.updated_at = now.clone(); + s.events.push(event); + if s.events.len() > MAX_EVENTS_PER_SESSION { + let n = s.events.len() - MAX_EVENTS_PER_SESSION; + s.events.drain(..n); + } + save_sessions(app_data_dir, &sessions)?; + return Ok(sessions[i].clone()); + } + + let session_id = uuid::Uuid::new_v4().to_string(); + let session = Session { + id: session_id.clone(), + project_id: project_id.to_string(), + created_at: now.clone(), + updated_at: now, + events: vec![event], + }; + sessions.push(session.clone()); + sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + if sessions.len() > MAX_SESSIONS_PER_PROJECT * 10 { + sessions.truncate(MAX_SESSIONS_PER_PROJECT * 10); + } + save_sessions(app_data_dir, &sessions)?; + Ok(session) +} diff --git a/src-tauri/src/tx/limits.rs b/src-tauri/src/tx/limits.rs new file mode 100644 index 0000000..d7e9720 --- /dev/null +++ b/src-tauri/src/tx/limits.rs @@ -0,0 +1,107 @@ +//! v2.3.4 Safe Guards: preflight checks and limits. + +use std::path::Path; + +use crate::types::{Action, ActionKind}; +use crate::tx::safe_join; + +pub const MAX_ACTIONS: usize = 50; +pub const MAX_FILES_TOUCHED: usize = 50; +pub const MAX_BYTES_WRITTEN: u64 = 2 * 1024 * 1024; // 2MB +pub const MAX_DIRS_CREATED: usize = 20; +pub const MAX_FILE_SIZE_UPDATE: u64 = 1024 * 1024; // 1MB for UpdateFile content + +static FORBIDDEN_PREFIXES: &[&str] = &[ + ".git/", + "node_modules/", + "target/", + "dist/", + "build/", + ".next/", + ".cache/", + "coverage/", +]; + +pub const PRECHECK_DENIED: &str = "PRECHECK_DENIED"; +pub const LIMIT_EXCEEDED: &str = "LIMIT_EXCEEDED"; +pub const PATH_FORBIDDEN: &str = "PATH_FORBIDDEN"; + +/// Preflight: validate paths and limits. Returns Err((message, error_code)) on failure. +pub fn preflight_actions(root: &Path, actions: &[Action]) -> Result<(), (String, String)> { + if actions.len() > MAX_ACTIONS { + return Err(( + format!("Превышен лимит действий: {} (макс. {})", actions.len(), MAX_ACTIONS), + LIMIT_EXCEEDED.into(), + )); + } + + let mut files_touched = 0usize; + let mut dirs_created = 0usize; + let mut total_bytes: u64 = 0; + + for a in actions { + let rel = a.path.replace('\\', "/"); + if rel.contains("..") { + return Err(("Путь не должен содержать ..".into(), PATH_FORBIDDEN.into())); + } + if Path::new(&rel).is_absolute() { + return Err(("Абсолютные пути запрещены".into(), PATH_FORBIDDEN.into())); + } + + for prefix in FORBIDDEN_PREFIXES { + if rel.starts_with(prefix) || rel == prefix.trim_end_matches('/') { + return Err(( + format!("Запрещённая зона: {}", rel), + PATH_FORBIDDEN.into(), + )); + } + } + + let abs = safe_join(root, &rel).map_err(|e| (e, PATH_FORBIDDEN.into()))?; + if abs.exists() && abs.is_symlink() { + return Err(("Симлинки не поддерживаются".into(), PRECHECK_DENIED.into())); + } + + match a.kind { + ActionKind::CreateFile | ActionKind::UpdateFile => { + files_touched += 1; + let len = a.content.as_deref().map(|s| s.len() as u64).unwrap_or(0); + if a.kind == ActionKind::UpdateFile && len > MAX_FILE_SIZE_UPDATE { + return Err(( + format!("Файл для обновления слишком большой: {} байт", len), + LIMIT_EXCEEDED.into(), + )); + } + total_bytes += len; + } + ActionKind::CreateDir => { + dirs_created += 1; + } + ActionKind::DeleteFile => { + files_touched += 1; + } + ActionKind::DeleteDir => {} + } + } + + if files_touched > MAX_FILES_TOUCHED { + return Err(( + format!("Превышен лимит файлов: {} (макс. {})", files_touched, MAX_FILES_TOUCHED), + LIMIT_EXCEEDED.into(), + )); + } + if dirs_created > MAX_DIRS_CREATED { + return Err(( + format!("Превышен лимит создаваемых папок: {} (макс. {})", dirs_created, MAX_DIRS_CREATED), + LIMIT_EXCEEDED.into(), + )); + } + if total_bytes > MAX_BYTES_WRITTEN { + return Err(( + format!("Превышен лимит объёма записи: {} байт (макс. {})", total_bytes, MAX_BYTES_WRITTEN), + LIMIT_EXCEEDED.into(), + )); + } + + Ok(()) +} diff --git a/src-tauri/src/tx/mod.rs b/src-tauri/src/tx/mod.rs new file mode 100644 index 0000000..7bed703 --- /dev/null +++ b/src-tauri/src/tx/mod.rs @@ -0,0 +1,264 @@ +mod limits; +mod store; + +pub use limits::preflight_actions; +pub use store::{clear_redo, get_undo_redo_state, pop_redo, pop_undo, push_redo, push_undo}; + +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use chrono::Utc; +use serde_json::json; +use tauri::{AppHandle, Manager}; + +use crate::types::{Action, ActionKind, TxManifest, TxTouchedItem}; + +pub fn user_data_dir(app: &AppHandle) -> PathBuf { + app.path().app_data_dir().expect("app_data_dir") +} + +pub fn history_dir(app: &AppHandle) -> PathBuf { + user_data_dir(app).join("history") +} + +pub fn tx_dir(app: &AppHandle, tx_id: &str) -> PathBuf { + history_dir(app).join(tx_id) +} + +pub fn tx_manifest_path(app: &AppHandle, tx_id: &str) -> PathBuf { + tx_dir(app, tx_id).join("manifest.json") +} + +pub fn tx_before_dir(app: &AppHandle, tx_id: &str) -> PathBuf { + tx_dir(app, tx_id).join("before") +} + +pub fn ensure_history(app: &AppHandle) -> io::Result<()> { + fs::create_dir_all(history_dir(app))?; + Ok(()) +} + +pub fn new_tx_id() -> String { + format!("tx-{}", Utc::now().format("%Y%m%d-%H%M%S-%3f")) +} + +pub fn write_manifest(app: &AppHandle, manifest: &TxManifest) -> io::Result<()> { + let tx_id = &manifest.tx_id; + fs::create_dir_all(tx_dir(app, tx_id))?; + let p = tx_manifest_path(app, tx_id); + let bytes = serde_json::to_vec_pretty(manifest).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + fs::write(p, bytes)?; + Ok(()) +} + +pub fn read_manifest(app: &AppHandle, tx_id: &str) -> io::Result { + let p = tx_manifest_path(app, tx_id); + let bytes = fs::read(p)?; + serde_json::from_slice(&bytes).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) +} + +#[allow(dead_code)] +pub fn set_latest_tx(app: &AppHandle, tx_id: &str) -> io::Result<()> { + let p = history_dir(app).join("latest.json"); + let bytes = serde_json::to_vec_pretty(&json!({ "txId": tx_id })).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + fs::write(p, bytes)?; + Ok(()) +} + +#[allow(dead_code)] +pub fn clear_latest_tx(app: &AppHandle) -> io::Result<()> { + let p = history_dir(app).join("latest.json"); + let _ = fs::remove_file(p); + Ok(()) +} + +#[allow(dead_code)] +pub fn get_latest_tx(app: &AppHandle) -> Option { + let p = history_dir(app).join("latest.json"); + let bytes = fs::read(p).ok()?; + let v: serde_json::Value = serde_json::from_slice(&bytes).ok()?; + v.get("txId")?.as_str().map(|s| s.to_string()) +} + +/// Safe join: root + relative (forbids absolute and "..") +pub fn safe_join(root: &Path, rel: &str) -> Result { + let rp = Path::new(rel); + if rp.is_absolute() { + return Err("absolute paths forbidden".into()); + } + if rel.contains("..") { + return Err("path traversal forbidden".into()); + } + Ok(root.join(rp)) +} + +/// Snapshot: only copy existed files to before/; build touched (rel_path, kind, existed, bytes). +pub fn snapshot_before( + app: &AppHandle, + tx_id: &str, + root: &Path, + rel_paths: &[String], +) -> Result, String> { + let before = tx_before_dir(app, tx_id); + fs::create_dir_all(&before).map_err(|e| e.to_string())?; + + let mut touched = vec![]; + + for rel in rel_paths { + let abs = safe_join(root, rel)?; + if abs.exists() && !abs.is_symlink() { + if abs.is_file() { + let bytes = fs::metadata(&abs).map(|m| m.len()).unwrap_or(0); + let dst = safe_join(&before, rel)?; + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::copy(&abs, &dst).map_err(|e| e.to_string())?; + touched.push(TxTouchedItem { + rel_path: rel.clone(), + kind: "file".into(), + existed: true, + bytes, + }); + } else if abs.is_dir() { + touched.push(TxTouchedItem { + rel_path: rel.clone(), + kind: "dir".into(), + existed: true, + bytes: 0, + }); + } + } else { + touched.push(TxTouchedItem { + rel_path: rel.clone(), + kind: if rel.ends_with('/') || rel.is_empty() { "dir".into() } else { "file".into() }, + existed: false, + bytes: 0, + }); + } + } + + Ok(touched) +} + +/// Rollback tx: existed file -> restore from before; created file/dir -> remove; existed dir -> skip. +pub fn rollback_tx(app: &AppHandle, tx_id: &str) -> Result<(), String> { + let mut manifest = read_manifest(app, tx_id).map_err(|e| e.to_string())?; + let root = PathBuf::from(manifest.root_path.clone()); + let before = tx_before_dir(app, tx_id); + + let items: Vec<(String, String, bool)> = if !manifest.touched.is_empty() { + manifest.touched.iter().map(|t| (t.rel_path.clone(), t.kind.clone(), t.existed)).collect() + } else if let Some(ref snap) = manifest.snapshot_items { + snap.iter().map(|s| (s.rel_path.clone(), s.kind.clone(), s.existed)).collect() + } else { + return Err("manifest has no touched or snapshot_items".into()); + }; + + for (rel, kind, existed) in items { + let abs = safe_join(&root, &rel)?; + let src = safe_join(&before, &rel).ok(); + + if existed { + if kind == "file" { + if let Some(ref s) = src { + if s.is_file() { + if let Some(parent) = abs.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::copy(s, &abs).map_err(|e| e.to_string())?; + } + } + } + // existed dir: skip (nothing to restore) + } else { + if abs.is_file() { + let _ = fs::remove_file(&abs); + } + if abs.is_dir() { + let _ = fs::remove_dir_all(&abs); + } + } + } + + manifest.status = "rolled_back".into(); + let _ = write_manifest(app, &manifest); + Ok(()) +} + +/// Collect unique rel_paths from actions (for snapshot). +pub fn collect_rel_paths(actions: &[Action]) -> Vec { + let mut paths: Vec = actions.iter().map(|a| a.path.clone()).collect(); + paths.sort(); + paths.dedup(); + paths +} + +/// PAPAYU_NORMALIZE_EOL=lf — нормализовать \r\n→\n, добавить trailing newline. +pub fn normalize_content_for_write(content: &str, _path: &Path) -> String { + let mode = std::env::var("PAPAYU_NORMALIZE_EOL") + .map(|s| s.trim().to_lowercase()) + .unwrap_or_else(|_| "keep".to_string()); + if mode != "lf" { + return content.to_string(); + } + let mut s = content.replace("\r\n", "\n").replace('\r', "\n"); + if !s.is_empty() && !s.ends_with('\n') { + s.push('\n'); + } + s +} + +/// Apply a single action to disk (v2.3.3: for atomic apply + rollback on first failure). +pub fn apply_one_action(root: &Path, action: &Action) -> Result<(), String> { + let full = safe_join(root, &action.path)?; + match action.kind { + ActionKind::CreateFile | ActionKind::UpdateFile => { + if let Some(p) = full.parent() { + fs::create_dir_all(p).map_err(|e| e.to_string())?; + } + let content = action.content.as_deref().unwrap_or(""); + let normalized = normalize_content_for_write(content, &full); + fs::write(&full, normalized).map_err(|e| e.to_string())?; + } + ActionKind::CreateDir => { + fs::create_dir_all(&full).map_err(|e| e.to_string())?; + } + ActionKind::DeleteFile => { + if full.exists() { + fs::remove_file(&full).map_err(|e| e.to_string())?; + } + } + ActionKind::DeleteDir => { + if full.is_dir() { + fs::remove_dir_all(&full).map_err(|e| e.to_string())?; + } + } + } + Ok(()) +} + +/// Порядок применения: CREATE_DIR → CREATE_FILE/UPDATE_FILE → DELETE_FILE → DELETE_DIR. +pub fn sort_actions_for_apply(actions: &mut [Action]) { + fn order(k: &ActionKind) -> u8 { + match k { + ActionKind::CreateDir => 0, + ActionKind::CreateFile | ActionKind::UpdateFile => 1, + ActionKind::DeleteFile => 2, + ActionKind::DeleteDir => 3, + } + } + actions.sort_by_key(|a| (order(&a.kind), a.path.clone())); +} + +/// Apply actions to disk (create/update/delete files and dirs). +/// Actions are sorted: CREATE_DIR → CREATE/UPDATE → DELETE_FILE → DELETE_DIR. +pub fn apply_actions_to_disk(root: &Path, actions: &[Action]) -> Result<(), String> { + let mut sorted: Vec = actions.to_vec(); + sort_actions_for_apply(&mut sorted); + for a in &sorted { + apply_one_action(root, a)?; + } + Ok(()) +} diff --git a/src-tauri/src/tx/store.rs b/src-tauri/src/tx/store.rs new file mode 100644 index 0000000..5a1d6fd --- /dev/null +++ b/src-tauri/src/tx/store.rs @@ -0,0 +1,80 @@ +//! Undo/redo stack in userData/history/state.json + +use std::fs; +use std::io; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use tauri::AppHandle; + +use super::history_dir; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct UndoRedoStateFile { + pub undo_stack: Vec, + pub redo_stack: Vec, +} + +fn state_path(app: &AppHandle) -> PathBuf { + history_dir(app).join("state.json") +} + +fn load_state(app: &AppHandle) -> UndoRedoStateFile { + let p = state_path(app); + if let Ok(bytes) = fs::read(&p) { + if let Ok(s) = serde_json::from_slice::(&bytes) { + return s; + } + } + UndoRedoStateFile::default() +} + +fn save_state(app: &AppHandle, state: &UndoRedoStateFile) -> io::Result<()> { + let p = state_path(app); + if let Some(parent) = p.parent() { + fs::create_dir_all(parent)?; + } + let bytes = serde_json::to_vec_pretty(state).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + fs::write(p, bytes) +} + +pub fn push_undo(app: &AppHandle, tx_id: String) -> io::Result<()> { + let mut state = load_state(app); + state.undo_stack.push(tx_id); + state.redo_stack.clear(); + save_state(app, &state) +} + +pub fn pop_undo(app: &AppHandle) -> Option { + let mut state = load_state(app); + let tx_id = state.undo_stack.pop()?; + save_state(app, &state).ok()?; + Some(tx_id) +} + +pub fn push_redo(app: &AppHandle, tx_id: String) -> io::Result<()> { + let mut state = load_state(app); + state.redo_stack.push(tx_id); + save_state(app, &state) +} + +pub fn pop_redo(app: &AppHandle) -> Option { + let mut state = load_state(app); + let tx_id = state.redo_stack.pop()?; + save_state(app, &state).ok()?; + Some(tx_id) +} + +pub fn clear_redo(app: &AppHandle) -> io::Result<()> { + let mut state = load_state(app); + state.redo_stack.clear(); + save_state(app, &state) +} + +pub fn get_undo_redo_state(app: &AppHandle) -> (bool, bool) { + let state = load_state(app); + ( + !state.undo_stack.is_empty(), + !state.redo_stack.is_empty(), + ) +} diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs new file mode 100644 index 0000000..5a275cd --- /dev/null +++ b/src-tauri/src/types.rs @@ -0,0 +1,509 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Action { + pub kind: ActionKind, + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ActionKind { + CreateFile, + CreateDir, + UpdateFile, + DeleteFile, + DeleteDir, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplyPayload { + pub root_path: String, + pub actions: Vec, + #[serde(default)] + pub auto_check: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + /// v2.4.2: обязательное подтверждение перед apply + #[serde(default)] + pub user_confirmed: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplyResult { + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub applied_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub failed_at: Option, // v2.3.3: index where apply failed (before rollback) + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxTouchedItem { + pub rel_path: String, + pub kind: String, // "file" | "dir" + pub existed: bool, + pub bytes: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxManifest { + pub tx_id: String, + pub root_path: String, + pub created_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + pub status: String, // "pending" | "committed" | "rolled_back" + #[serde(default)] + pub applied_actions: Vec, + #[serde(default)] + pub touched: Vec, + #[serde(default)] + pub auto_check: bool, + /// Legacy: old manifests had snapshot_items only + #[serde(default, skip_serializing_if = "Option::is_none")] + pub snapshot_items: Option>, +} + +/// Legacy alias for rollback reading old manifests +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxSnapshotItem { + pub rel_path: String, + pub kind: String, + pub existed: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UndoResult { + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UndoAvailableResult { + pub ok: bool, + pub available: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RedoResult { + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UndoRedoState { + pub undo_available: bool, + pub redo_available: bool, +} + +/// v2.4: action with metadata for plan +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActionItem { + pub id: String, + pub kind: ActionKind, + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + pub summary: String, + pub rationale: String, + pub tags: Vec, + pub risk: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActionPlan { + pub plan_id: String, + pub root_path: String, + pub title: String, + pub actions: Vec, + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GenerateActionsPayload { + pub path: String, + pub selected: Vec, + pub mode: String, // "safe" | "balanced" +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiffItem { + pub kind: String, + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub old_content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub new_content: Option, + /// v2.4.2: BLOCKED — защищённый/не-текстовый файл + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PreviewResult { + pub diffs: Vec, + pub summary: String, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyzePayload { + pub paths: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Finding { + pub title: String, + pub details: String, + pub path: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recommendation { + pub title: String, + pub details: String, + pub priority: Option, + pub effort: Option, + pub impact: Option, +} + +/// v2.9.2: сигнал по проекту (категория + уровень для recommended_pack_ids) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectSignal { + pub category: String, // "security" | "quality" | "structure" + pub level: String, // "warn" | "high" | "critical" +} + +/// v2.9.1: группа действий (readme, gitignore, tests, …) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActionGroup { + pub id: String, + pub title: String, + pub description: String, + pub actions: Vec, +} + +/// v2.9.2: пакет улучшений (security, quality, structure) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FixPack { + pub id: String, + pub title: String, + pub description: String, + pub group_ids: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyzeReport { + pub path: String, + pub narrative: String, + pub findings: Vec, + pub recommendations: Vec, + pub actions: Vec, + #[serde(default)] + pub action_groups: Vec, + #[serde(default)] + pub fix_packs: Vec, + #[serde(default)] + pub recommended_pack_ids: Vec, + /// v2.4.5: прикреплённые файлы, переданные при анализе (контекст для UI/планировщика) + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub attached_files: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchPayload { + pub paths: Vec, + pub confirm_apply: bool, + pub auto_check: bool, + pub selected_actions: Option>, + /// v2.4.2: передаётся в ApplyPayload при confirm_apply + #[serde(default)] + pub user_confirmed: bool, + /// v2.4.5: прикреплённые файлы для контекста при анализе + #[serde(default)] + pub attached_files: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchEvent { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub report: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub apply_result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub undo_available: Option, +} + +/// v2.9.3: транзакционное применение (path + actions + auto_check) +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionSpec { + pub path: String, + pub actions: Vec, + pub auto_check: bool, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionResult { + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, + pub applied_count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UndoStatus { + pub available: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, +} + +/// v3.0: сообщение агента (user / system / assistant) +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentMessage { + pub role: String, + pub text: String, +} + +/// v3.0: план агента (propose_actions) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentPlan { + pub ok: bool, + pub summary: String, + pub actions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + /// JSON плана для передачи в Apply (при Plan-режиме). + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_json: Option, + /// Собранный контекст для передачи в Apply вместе с plan_json. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_context: Option, +} + +/// v3.1: опции применения (auto_check). v2.4.2: user_confirmed для apply_actions_tx. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplyOptions { + pub auto_check: bool, + #[serde(default)] + pub user_confirmed: bool, +} + +/// v3.1: результат этапа проверки (verify / build / smoke) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckStageResult { + pub stage: String, + pub ok: bool, + pub output: String, +} + +/// v3.1: результат транзакционного apply с авто-проверкой и откатом +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplyTxResult { + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_id: Option, + pub applied: bool, + pub rolled_back: bool, + pub checks: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +/// v3.2: результат генерации действий из отчёта (generate_actions_from_report) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GenerateActionsResult { + pub ok: bool, + pub actions: Vec, + #[serde(default)] + pub skipped: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +// --- v2.4 Agentic Loop --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgenticConstraints { + pub auto_check: bool, + pub max_attempts: u8, + pub max_actions: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgenticRunRequest { + pub path: String, + pub goal: String, + pub constraints: AgenticConstraints, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckItem { + pub name: String, + pub ok: bool, + pub output: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerifyResult { + pub ok: bool, + pub checks: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AttemptResult { + pub attempt: u8, + pub plan: String, + pub actions: Vec, + pub preview: PreviewResult, + pub apply: ApplyTxResult, + pub verify: VerifyResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgenticRunResult { + pub ok: bool, + pub attempts: Vec, + pub final_summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, +} + +// --- Тренды и рекомендации (мониторинг не реже раз в месяц) --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrendsRecommendation { + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrendsResult { + pub last_updated: String, + pub recommendations: Vec, + /// true если прошло >= 30 дней с last_updated — рекомендуется обновить + pub should_update: bool, +} + +// --- Projects & sessions (v2.5: entities, history, profiles) --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub path: String, + pub name: String, + pub created_at: String, +} + +/// v2.5: сохранённые настройки проекта (store) +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProjectSettings { + pub project_id: String, + #[serde(default)] + pub auto_check: bool, + #[serde(default)] + pub max_attempts: u8, + #[serde(default)] + pub max_actions: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub goal_template: Option, +} + +// --- v2.4.3: detected profile (by path) --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ProjectType { + ReactVite, + NextJs, + Node, + Rust, + Python, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectLimits { + pub max_files: u32, + pub timeout_sec: u32, + pub max_actions_per_tx: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectProfile { + pub path: String, + pub project_type: ProjectType, + pub safe_mode: bool, + pub max_attempts: u32, + pub goal_template: String, + pub limits: ProjectLimits, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionEvent { + pub kind: String, // "message" | "analyze" | "agentic_run" | "apply" + pub role: Option, + pub text: Option, + pub at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, + pub project_id: String, + pub created_at: String, + pub updated_at: String, + #[serde(default)] + pub events: Vec, +} diff --git a/src-tauri/src/verify.rs b/src-tauri/src/verify.rs new file mode 100644 index 0000000..925b79d --- /dev/null +++ b/src-tauri/src/verify.rs @@ -0,0 +1,200 @@ +//! v2.4: verify_project — проверка сборки/типов после apply (allowlisted, timeout 60s). +//! v2.4.5: allowlist команд загружается из config/verify_allowlist.json (или встроенный дефолт). + +use std::collections::HashMap; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +use crate::types::{CheckItem, VerifyResult}; + +/// Одна разрешённая команда из конфига. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct VerifyAllowlistEntry { + pub exe: String, + pub args: Vec, + pub name: String, + #[serde(default)] + pub timeout_sec: Option, +} + +fn default_timeout() -> u64 { + 60 +} + +fn load_verify_allowlist() -> HashMap> { + const DEFAULT_JSON: &str = include_str!("../config/verify_allowlist.json"); + serde_json::from_str(DEFAULT_JSON).unwrap_or_else(|_| HashMap::new()) +} + +fn run_check(cwd: &Path, exe: &str, args: &[&str], name: &str, timeout_secs: u64) -> CheckItem { + let timeout = Duration::from_secs(timeout_secs); + let mut cmd = Command::new(exe); + cmd.args(args) + .current_dir(cwd) + .env("CI", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let (ok, output_str) = match cmd.spawn() { + Ok(mut child) => { + let start = Instant::now(); + let result = loop { + if start.elapsed() > timeout { + let _ = child.kill(); + let _ = child.wait(); + break (false, format!("TIMEOUT ({}s)", timeout_secs)); + } + match child.try_wait() { + Ok(Some(_status)) => { + let out = child.wait_with_output(); + let (success, combined) = match out { + Ok(o) => { + let out_str = String::from_utf8_lossy(&o.stdout); + let err_str = String::from_utf8_lossy(&o.stderr); + let combined = format!("{}{}", out_str, err_str); + let combined = if combined.len() > 8000 { + format!("{}…", &combined[..8000]) + } else { + combined + }; + (o.status.success(), combined) + } + Err(e) => (false, e.to_string()), + }; + break (success, combined); + } + Ok(None) => { + thread::sleep(Duration::from_millis(100)); + } + Err(e) => break (false, e.to_string()), + } + }; + result + } + Err(e) => (false, e.to_string()), + }; + + CheckItem { + name: name.to_string(), + ok, + output: output_str, + } +} + +/// Определение типа проекта по наличию файлов в корне. +fn project_type(root: &Path) -> &'static str { + if root.join("Cargo.toml").exists() { + return "rust"; + } + if root.join("package.json").exists() { + return "node"; + } + if root.join("setup.py").exists() || root.join("pyproject.toml").exists() { + return "python"; + } + "unknown" +} + +/// Выполняет одну команду из allowlist (exe + args из конфига). +fn run_check_from_entry(cwd: &Path, entry: &VerifyAllowlistEntry) -> CheckItem { + let timeout = entry.timeout_sec.unwrap_or_else(default_timeout); + let args: Vec<&str> = entry.args.iter().map(|s| s.as_str()).collect(); + run_check(cwd, &entry.exe, &args, &entry.name, timeout) +} + +/// v2.4: проверка проекта после apply. Allowlist из config/verify_allowlist.json. +pub fn verify_project(path: &str) -> VerifyResult { + let root = Path::new(path); + if !root.exists() || !root.is_dir() { + return VerifyResult { + ok: false, + checks: vec![], + error: Some("path not found".to_string()), + error_code: Some("PATH_NOT_FOUND".into()), + }; + } + + let pt = project_type(root); + let allowlist = load_verify_allowlist(); + let mut checks: Vec = vec![]; + + match pt { + "rust" => { + if let Some(entries) = allowlist.get("rust") { + if let Some(entry) = entries.first() { + checks.push(run_check_from_entry(root, entry)); + } + } + if checks.is_empty() { + checks.push(run_check(root, "cargo", &["check"], "cargo check", 60)); + } + } + "node" => { + let (exe, args, name): (String, Vec, String) = { + let pkg = root.join("package.json"); + if pkg.exists() { + if let Ok(s) = std::fs::read_to_string(&pkg) { + if s.contains("\"test\"") { + ("npm".into(), vec!["run".into(), "-s".into(), "test".into()], "npm test".into()) + } else if s.contains("\"build\"") { + ("npm".into(), vec!["run".into(), "-s".into(), "build".into()], "npm run build".into()) + } else if s.contains("\"lint\"") { + ("npm".into(), vec!["run".into(), "-s".into(), "lint".into()], "npm run lint".into()) + } else { + ("npm".into(), vec!["run".into(), "-s".into(), "build".into()], "npm run build".into()) + } + } else { + ("npm".into(), vec!["run".into(), "-s".into(), "build".into()], "npm run build".into()) + } + } else { + ("npm".into(), vec!["run".into(), "-s".into(), "build".into()], "npm run build".into()) + } + }; + let allowed = allowlist.get("node").and_then(|entries| { + entries.iter().find(|e| e.exe == exe && e.args == args) + }); + let timeout = allowed.and_then(|e| e.timeout_sec).unwrap_or(60); + let name_str = allowed.map(|e| e.name.as_str()).unwrap_or(name.as_str()); + let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + checks.push(run_check(root, &exe, &args_ref, name_str, timeout)); + } + "python" => { + if let Some(entries) = allowlist.get("python") { + if let Some(entry) = entries.first() { + checks.push(run_check_from_entry(root, entry)); + } + } + if checks.is_empty() { + checks.push(run_check( + root, + "python3", + &["-m", "compileall", ".", "-q"], + "python -m compileall", + 60, + )); + } + } + _ => { + return VerifyResult { + ok: true, + checks: vec![], + error: None, + error_code: None, + }; + } + } + + let ok = checks.iter().all(|c| c.ok); + VerifyResult { + ok, + checks, + error: if ok { + None + } else { + Some("verify failed".to_string()) + }, + error_code: if ok { None } else { Some("VERIFY_FAILED".into()) }, + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..98437b7 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "productName": "PAPA YU", + "version": "2.4.4", + "identifier": "com.papa-yu.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5173", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "PAPA YU", + "label": "main", + "width": 1024, + "height": 720, + "minWidth": 1024, + "minHeight": 720, + "resizable": true + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "icon": ["icons/icon.png"], + "resources": [], + "externalBin": [], + "copyright": "", + "category": "DeveloperTool", + "shortDescription": "PAPA YU", + "longDescription": "PAPA YU — анализ проекта и автоматические исправления" + }, + "plugins": {} +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..434c451 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,90 @@ +import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom"; +import Tasks from "./pages/Tasks"; +import Dashboard from "./pages/Dashboard"; + +function Layout({ children }: { children: React.ReactNode }) { + return ( +
+
+ PAPAYU + + PAPA YU + + +
+
{children}
+
+ ); +} + +export default function App() { + return ( + + + + } /> + } /> + + + + ); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..424ee45 --- /dev/null +++ b/src/index.css @@ -0,0 +1,179 @@ +:root { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-primary-light: #eff6ff; + --color-secondary: #0d9488; + --color-secondary-hover: #0f766e; + --color-accent: #7c3aed; + --color-accent-hover: #6d28d9; + --color-success: #16a34a; + --color-success-hover: #15803d; + --color-danger: #dc2626; + --color-danger-hover: #b91c1c; + --color-warning: #f59e0b; + --color-text: #0f172a; + --color-text-muted: #64748b; + --color-text-soft: #94a3b8; + --color-surface: #ffffff; + --color-bg: #f1f5f9; + --color-bg-warm: #f8fafc; + --color-border: #e2e8f0; + --color-border-strong: #cbd5e1; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.05); + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06); + --transition: 0.2s ease; +} + +/* Dark Theme */ +[data-theme="dark"] { + --color-primary: #3b82f6; + --color-primary-hover: #60a5fa; + --color-primary-light: #1e3a5f; + --color-secondary: #14b8a6; + --color-secondary-hover: #2dd4bf; + --color-accent: #a78bfa; + --color-accent-hover: #c4b5fd; + --color-success: #22c55e; + --color-success-hover: #4ade80; + --color-danger: #ef4444; + --color-danger-hover: #f87171; + --color-warning: #fbbf24; + --color-text: #f1f5f9; + --color-text-muted: #94a3b8; + --color-text-soft: #64748b; + --color-surface: #1e293b; + --color-bg: #0f172a; + --color-bg-warm: #1e293b; + --color-border: #334155; + --color-border-strong: #475569; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3); + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--color-text); + background: linear-gradient(160deg, var(--color-bg-warm) 0%, var(--color-border) 50%, var(--color-bg) 100%); + min-height: 100vh; + transition: background var(--transition), color var(--transition); +} + +[data-theme="dark"] body { + background: linear-gradient(160deg, var(--color-bg-warm) 0%, #0f172a 50%, var(--color-bg) 100%); +} + +#root { + min-height: 100vh; +} + +button { + cursor: pointer; + font: inherit; + transition: background-color var(--transition), transform var(--transition), box-shadow var(--transition); +} +button:hover:not(:disabled) { + transform: translateY(-1px); +} +button:active:not(:disabled) { + transform: translateY(0); +} +button:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +input, +textarea { + font: inherit; + transition: border-color var(--transition), box-shadow var(--transition); +} +input:focus, +textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); +} + +.card { + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + border: 1px solid var(--color-border); +} + +/* Блок выбора папки на странице Задачи — всегда видим */ +.tasks-sources[data-section="path-selection"] { + display: block !important; + visibility: visible !important; +} + +/* Theme Toggle */ +.theme-toggle { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + font-size: 13px; + color: var(--color-text-muted); + transition: all var(--transition); +} + +.theme-toggle:hover { + background: var(--color-bg-warm); + border-color: var(--color-border-strong); + color: var(--color-text); +} + +.theme-toggle-icon { + font-size: 16px; +} + +/* Dark theme specific overrides */ +[data-theme="dark"] input, +[data-theme="dark"] textarea { + background: var(--color-surface); + color: var(--color-text); + border-color: var(--color-border); +} + +[data-theme="dark"] input::placeholder, +[data-theme="dark"] textarea::placeholder { + color: var(--color-text-soft); +} + +[data-theme="dark"] input:focus, +[data-theme="dark"] textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25); +} + +[data-theme="dark"] select { + background: var(--color-surface); + color: var(--color-text); + border-color: var(--color-border); +} + +[data-theme="dark"] code, +[data-theme="dark"] pre { + background: #0f172a; + color: #e2e8f0; +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts new file mode 100644 index 0000000..70114ad --- /dev/null +++ b/src/lib/tauri.ts @@ -0,0 +1,221 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + Action, + AgenticRunRequest, + AgenticRunResult, + AnalyzeReport, + ApplyTxResult, + BatchEvent, + GenerateActionsResult, + PreviewResult, + ProjectProfile, + Session, + TrendsResult, + UndoStatus, + VerifyResult, +} from "./types"; + +export interface UndoRedoState { + undo_available: boolean; + redo_available: boolean; +} + +export interface RunBatchPayload { + paths: string[]; + confirm_apply: boolean; + auto_check: boolean; + selected_actions?: Action[]; + user_confirmed?: boolean; + attached_files?: string[]; +} + +export interface ApplyActionsTxOptions { + auto_check: boolean; + user_confirmed: boolean; +} + +export interface ProjectItem { + id: string; + path: string; +} + +export interface AddProjectResult { + id: string; +} + +export interface UndoLastResult { + ok: boolean; + error_code?: string; + error?: string; +} + +export async function getUndoRedoState(): Promise { + return invoke("get_undo_redo_state_cmd"); +} + +export async function getUndoStatus(): Promise { + return invoke("undo_status").catch(() => ({ available: false } as UndoStatus)); +} + +export async function getFolderLinks(): Promise<{ paths: string[] }> { + return invoke<{ paths: string[] }>("get_folder_links"); +} + +export async function setFolderLinks(paths: string[]): Promise { + return invoke("set_folder_links", { links: { paths } }); +} + +export async function getProjectProfile(path: string): Promise { + return invoke("get_project_profile", { path }); +} + +export async function runBatchCmd(payload: RunBatchPayload): Promise { + return invoke("run_batch_cmd", { payload }); +} + +/** Предпросмотр diff для actions (CREATE/UPDATE/DELETE) без записи на диск. */ +export async function previewActions(rootPath: string, actions: Action[]): Promise { + return invoke("preview_actions_cmd", { + payload: { + root_path: rootPath, + actions, + auto_check: null, + label: null, + user_confirmed: false, + }, + }); +} + +export async function applyActionsTx( + path: string, + actions: Action[], + options: ApplyActionsTxOptions | boolean +): Promise { + const opts: ApplyActionsTxOptions = + typeof options === "boolean" + ? { auto_check: options, user_confirmed: true } + : options; + return invoke("apply_actions_tx", { + path, + actions, + options: opts, + }); +} + +export async function generateActionsFromReport( + path: string, + report: AnalyzeReport, + mode: string +): Promise { + return invoke("generate_actions_from_report", { + path, + report, + mode, + }); +} + +export async function agenticRun(payload: AgenticRunRequest): Promise { + return invoke("agentic_run", { payload }); +} + +export async function listProjects(): Promise { + return invoke("list_projects"); +} + +export async function addProject(path: string, name: string | null): Promise { + return invoke("add_project", { path, name }); +} + +export async function listSessions(projectId?: string): Promise { + return invoke("list_sessions", { projectId: projectId ?? null }); +} + +export async function appendSessionEvent( + projectId: string, + kind: string, + role: string, + text: string +): Promise { + return invoke("append_session_event", { + project_id: projectId, + kind, + role, + text, + }); +} + +export interface AgentPlanResult { + ok: boolean; + summary: string; + actions: Action[]; + error?: string; + error_code?: string; + plan_json?: string; + plan_context?: string; +} + +export async function proposeActions( + path: string, + reportJson: string, + userGoal: string, + designStyle?: string | null, + trendsContext?: string | null, + lastPlanJson?: string | null, + lastContext?: string | null +): Promise { + return invoke("propose_actions", { + path, + reportJson, + userGoal, + designStyle: designStyle ?? null, + trendsContext: trendsContext ?? null, + lastPlanJson: lastPlanJson ?? null, + lastContext: lastContext ?? null, + }); +} + +export async function undoLastTx(path: string): Promise { + return invoke("undo_last_tx", { path }); +} + +export async function undoLast(): Promise { + return invoke("undo_last"); +} + +export async function redoLast(): Promise { + return invoke("redo_last"); +} + +/** Проверка целостности проекта (типы, сборка, тесты). Вызывается автоматически после применений или вручную. */ +export async function verifyProject(path: string): Promise { + return invoke("verify_project", { path }); +} + +/** Тренды и рекомендации: последнее обновление и список. should_update === true если прошло >= 30 дней. */ +export async function getTrendsRecommendations(): Promise { + return invoke("get_trends_recommendations"); +} + +/** Обновить тренды и рекомендации (запрос к внешним ресурсам по allowlist). */ +export async function fetchTrendsRecommendations(): Promise { + return invoke("fetch_trends_recommendations"); +} + +// Settings export/import + +export interface ImportResult { + projects_imported: number; + profiles_imported: number; + sessions_imported: number; + folder_links_imported: number; +} + +/** Export all settings as JSON string */ +export async function exportSettings(): Promise { + return invoke("export_settings"); +} + +/** Import settings from JSON string */ +export async function importSettings(json: string, mode?: "replace" | "merge"): Promise { + return invoke("import_settings", { json, mode: mode ?? "merge" }); +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..2dfc132 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,207 @@ +export interface Action { + kind: string; + path: string; + content?: string; +} + +export interface Finding { + title: string; + details: string; + path?: string; +} + +export interface ActionGroup { + id: string; + title: string; + description: string; + actions: Action[]; +} + +export interface FixPack { + id: string; + title: string; + description: string; + group_ids: string[]; +} + +export interface AnalyzeReport { + path: string; + narrative: string; + findings: Finding[]; + recommendations: unknown[]; + actions: Action[]; + action_groups?: ActionGroup[]; + fix_packs?: FixPack[]; + recommended_pack_ids?: string[]; +} + +export interface DiffItem { + kind: string; + path: string; + old_content?: string; + new_content?: string; + /** v2.4.2: BLOCKED — защищённый/не-текстовый файл */ + summary?: string; +} + +export interface PreviewResult { + diffs: DiffItem[]; + summary: string; +} + +export interface ApplyResult { + ok: boolean; + tx_id?: string; + error?: string; + error_code?: string; +} + +/** v2.9.3: доступен ли откат транзакции */ +export interface UndoStatus { + available: boolean; + tx_id?: string; +} + +/** v3.0: план агента (propose_actions) */ +export interface AgentPlan { + ok: boolean; + summary: string; + actions: Action[]; + error?: string; + error_code?: string; + /** JSON плана для передачи в Apply */ + plan_json?: string; + /** Собранный контекст для Apply */ + plan_context?: string; +} + +/** Тренды и рекомендации (мониторинг не реже раз в месяц) */ +export interface TrendsRecommendation { + title: string; + summary?: string; + url?: string; + source?: string; +} + +export interface TrendsResult { + last_updated: string; + recommendations: TrendsRecommendation[]; + should_update: boolean; +} + +/** v3.1: результат apply_actions_tx с autocheck и откатом */ +export interface ApplyTxResult { + ok: boolean; + tx_id?: string | null; + applied: boolean; + rolled_back: boolean; + checks: { stage: string; ok: boolean; output: string }[]; + error?: string; + error_code?: string; +} + +/** v3.2: результат generate_actions_from_report */ +export interface GenerateActionsResult { + ok: boolean; + actions: Action[]; + skipped: string[]; + error?: string; + error_code?: string; +} + +/** v2.4: Agentic Loop */ +export interface AgenticConstraints { + auto_check: boolean; + max_attempts: number; + max_actions: number; +} + +export interface AgenticRunRequest { + path: string; + goal: string; + constraints: AgenticConstraints; +} + +export interface CheckItem { + name: string; + ok: boolean; + output: string; +} + +export interface VerifyResult { + ok: boolean; + checks: CheckItem[]; + error?: string; + error_code?: string; +} + +export interface AttemptResult { + attempt: number; + plan: string; + actions: Action[]; + preview: PreviewResult; + apply: ApplyTxResult; + verify: VerifyResult; +} + +export interface AgenticRunResult { + ok: boolean; + attempts: AttemptResult[]; + final_summary: string; + error?: string; + error_code?: string; +} + +/** v2.4.3: detected profile (by path) */ +export type ProjectType = "react_vite" | "next_js" | "node" | "rust" | "python" | "unknown"; + +export interface ProjectLimits { + max_files: number; + timeout_sec: number; + max_actions_per_tx: number; +} + +export interface ProjectProfile { + path: string; + project_type: ProjectType; + safe_mode: boolean; + max_attempts: number; + goal_template: string; + limits: ProjectLimits; +} + +export interface BatchEvent { + kind: string; + report?: AnalyzeReport; + preview?: PreviewResult; + apply_result?: ApplyResult; + message?: string; + undo_available?: boolean; +} + +export type ChatRole = "system" | "user" | "assistant"; + +export interface ChatMessage { + role: ChatRole; + text: string; + report?: AnalyzeReport; + preview?: PreviewResult; + applyResult?: ApplyResult; +} + +/** Событие сессии (agentic_run, message, analyze, apply) */ +export interface SessionEvent { + kind: string; + role?: string; + text?: string; + at: string; +} + +/** Сессия по проекту */ +export interface Session { + id: string; + project_id: string; + created_at: string; + updated_at: string; + events: SessionEvent[]; +} diff --git a/src/lib/useTheme.ts b/src/lib/useTheme.ts new file mode 100644 index 0000000..ebe87c5 --- /dev/null +++ b/src/lib/useTheme.ts @@ -0,0 +1,57 @@ +import { useState, useEffect, useCallback } from "react"; + +type Theme = "light" | "dark"; + +const STORAGE_KEY = "papa_yu_theme"; + +function getInitialTheme(): Theme { + if (typeof window === "undefined") return "light"; + + // Check localStorage first + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "dark" || stored === "light") { + return stored; + } + + // Check system preference + if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + return "dark"; + } + + return "light"; +} + +export function useTheme() { + const [theme, setThemeState] = useState(getInitialTheme); + + useEffect(() => { + // Apply theme to document + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem(STORAGE_KEY, theme); + }, [theme]); + + useEffect(() => { + // Listen for system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = (e: MediaQueryListEvent) => { + const stored = localStorage.getItem(STORAGE_KEY); + // Only auto-switch if user hasn't explicitly set a preference + if (!stored) { + setThemeState(e.matches ? "dark" : "light"); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); + + const toggleTheme = useCallback(() => { + setThemeState((prev) => (prev === "light" ? "dark" : "light")); + }, []); + + const setTheme = useCallback((newTheme: Theme) => { + setThemeState(newTheme); + }, []); + + return { theme, toggleTheme, setTheme, isDark: theme === "dark" }; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..9b67590 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..2c76fa8 --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,111 @@ +import { Link } from "react-router-dom"; + +const sectionStyle: React.CSSProperties = { + marginBottom: "24px", + padding: "20px 24px", + background: "#fff", + borderRadius: "var(--radius-lg)", + border: "1px solid var(--color-border)", + boxShadow: "var(--shadow-sm)", +}; + +const headingStyle: React.CSSProperties = { + marginBottom: "12px", + fontSize: "16px", + fontWeight: 700, + color: "#1e3a5f", + letterSpacing: "-0.01em", +}; + +const textStyle: React.CSSProperties = { + color: "var(--color-text)", + marginBottom: "8px", + lineHeight: 1.6, + fontSize: "14px", +}; + +const listStyle: React.CSSProperties = { + margin: "8px 0 0 0", + paddingLeft: "20px", + lineHeight: 1.7, + color: "var(--color-text-muted)", + fontSize: "13px", +}; + +export default function Dashboard() { + return ( +
+

+ Панель управления +

+ +
+

Настройки программы

+

+ PAPA YU — написание программ под ключ, анализ и исправление с улучшениями. Ниже отображаются параметры и подсказки по настройке. +

+
+ +
+

Подключение ИИ (LLM)

+

+ Рекомендации и задачи ИИ работают при заданных переменных окружения. Задайте их в файле .env или в скрипте запуска: +

+
    +
  • PAPAYU_LLM_API_URL — URL API (например OpenAI или Ollama)
  • +
  • PAPAYU_LLM_API_KEY — API-ключ (для OpenAI обязателен)
  • +
  • PAPAYU_LLM_MODEL — модель (например gpt-4o-mini, llama3.2)
  • +
+

+ Запуск с OpenAI: используйте скрипт start-with-openai.sh или задайте переменные вручную. +

+
+ +
+

Поведение по умолчанию

+

+ Для каждого проекта можно задать: +

+
    +
  • Автопроверка — проверка типов, сборки и тестов после применённых изменений (по умолчанию включена)
  • +
  • Максимум попыток агента при автоматическом исправлении (по умолчанию 2)
  • +
  • Максимум действий за одну транзакцию (по умолчанию 12)
  • +
+

+ Эти настройки применяются при работе с проектом во вкладке «Задачи» (профиль проекта). +

+
+ +
+

Тренды и рекомендации

+

+ Раздел «Тренды и рекомендации» в левой панели «Задач» загружает актуальные рекомендации по разработке. Обновление — не реже раза в 30 дней. Кнопка «Обновить тренды» подгружает новые данные. +

+
+ +

+ + Перейти в «Задачи» → + +

+
+ ); +} diff --git a/src/pages/Tasks.tsx b/src/pages/Tasks.tsx new file mode 100644 index 0000000..c03bdf8 --- /dev/null +++ b/src/pages/Tasks.tsx @@ -0,0 +1,2069 @@ +import { useState, useEffect, useRef } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { open } from "@tauri-apps/plugin-dialog"; +import { + getFolderLinks, + setFolderLinks as setFolderLinksBackend, + getProjectProfile, + runBatchCmd, + applyActionsTx as apiApplyActionsTx, + generateActionsFromReport, + agenticRun, + listProjects, + listSessions, + addProject, + appendSessionEvent, + proposeActions, + previewActions, + verifyProject, + getTrendsRecommendations, + fetchTrendsRecommendations, + exportSettings, + importSettings, +} from "@/lib/tauri"; +import { AgenticResult } from "@/pages/tasks/AgenticResult"; +import { useUndoRedo } from "@/pages/tasks/useUndoRedo"; +import { useTheme } from "@/lib/useTheme"; +import type { + Action, + ActionGroup, + AnalyzeReport, + ChatMessage, + DiffItem, + ProjectProfile, + ApplyTxResult, + AgenticRunRequest, + AgenticRunResult, + Session, + TrendsRecommendation, + TrendsResult, + VerifyResult, +} from "@/lib/types"; + +const STORAGE_LINKS = "papa_yu_folder_links"; + +function loadLocalLinks(): string[] { + try { + const s = localStorage.getItem(STORAGE_LINKS); + if (s) return JSON.parse(s); + } catch (_) {} + return []; +} + +function saveLocalLinks(paths: string[]) { + localStorage.setItem(STORAGE_LINKS, JSON.stringify(paths)); +} + +export default function Tasks() { + const [folderLinks, setFolderLinks] = useState(loadLocalLinks()); + const [input, setInput] = useState(""); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [lastReport, setLastReport] = useState(null); + const [selectedActions, setSelectedActions] = useState([]); + const [pendingPreview, setPendingPreview] = useState<{ path: string; actions: Action[]; diffs: DiffItem[] } | null>(null); + const [autoCheck, setAutoCheck] = useState(true); + const [lastPath, setLastPath] = useState(null); + const [lastReportJson, setLastReportJson] = useState(null); + const [pendingActions, setPendingActions] = useState(null); + const [pendingActionIdx, setPendingActionIdx] = useState>({}); + const [selectedFixGroupIds, setSelectedFixGroupIds] = useState>({}); + const [selectedPackIds, setSelectedPackIds] = useState>({}); + const [suggestedActions, setSuggestedActions] = useState([]); + const [selectedActionIdx, setSelectedActionIdx] = useState>({}); + const [isGeneratingActions, setIsGeneratingActions] = useState(false); + const [createOnlyMode, setCreateOnlyMode] = useState(true); + const [agenticRunning, setAgenticRunning] = useState(false); + const [agenticProgress, setAgenticProgress] = useState<{ stage: string; message: string; attempt: number } | null>(null); + const [agenticResult, setAgenticResult] = useState(null); + const [profile, setProfile] = useState(null); + const [attachedFiles, setAttachedFiles] = useState([]); + const [sessions, setSessions] = useState([]); + const [sessionsExpanded, setSessionsExpanded] = useState(false); + const [agentGoal, setAgentGoal] = useState(""); + const [verifyResult, setVerifyResult] = useState(null); + const [verifying, setVerifying] = useState(false); + const [designStyle, setDesignStyle] = useState(""); + const [trends, setTrends] = useState(null); + const [trendsLoading, setTrendsLoading] = useState(false); + const [applyProgressVisible, setApplyProgressVisible] = useState(false); + const [applyProgressLog, setApplyProgressLog] = useState([]); + const [applyResult, setApplyResult] = useState(null); + const applyingRef = useRef(false); + const [requestHistory, setRequestHistory] = useState<{ id: string; title: string; messages: ChatMessage[]; lastPath: string | null; lastReport: AnalyzeReport | null }[]>([]); + const [trendsModalOpen, setTrendsModalOpen] = useState(false); + const [selectedRecommendation, setSelectedRecommendation] = useState(null); + const [attachmentMenuOpen, setAttachmentMenuOpen] = useState(false); + const [lastPlanJson, setLastPlanJson] = useState(null); + const [lastPlanContext, setLastPlanContext] = useState(null); + + const { undoAvailable, redoAvailable, refreshUndoRedo, handleUndo, handleRedo, setUndoAvailable } = useUndoRedo(lastPath, { + setMessages, + setPendingPreview, + setLastReport, + }); + + const { toggleTheme, isDark } = useTheme(); + + useEffect(() => { + saveLocalLinks(folderLinks); + refreshUndoRedo(); + (async () => { + try { + const links = await getFolderLinks(); + if (links.paths?.length) setFolderLinks(links.paths); + } catch (_) {} + })(); + }, []); + + useEffect(() => { + if (!lastPath) { + setSessions([]); + return; + } + (async () => { + try { + const projects = await listProjects(); + const projectId = projects.find((p) => p.path === lastPath)?.id; + if (projectId) { + const list = await listSessions(projectId); + setSessions(list); + } else { + setSessions([]); + } + } catch (_) { + setSessions([]); + } + })(); + }, [lastPath]); + + useEffect(() => { + (async () => { + try { + const res = await getTrendsRecommendations(); + setTrends(res); + } catch (_) { + setTrends(null); + } + })(); + }, []); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setPendingPreview(null); + return; + } + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { + e.preventDefault(); + if (!loading) onAnalyze(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [loading]); + + useEffect(() => { + const unlisten = listen<{ stage: string; message: string; attempt: number }>("agentic_progress", (ev) => { + setAgenticProgress(ev.payload); + const stageToText: Record = { + analyze: "Сканирую проект…", + plan: "Составляю план исправлений…", + preview: "Показываю, что изменится…", + apply: "Применяю изменения…", + verify: "Проверяю сборку/типы…", + revert: "Обнаружены ошибки. Откатываю изменения…", + done: "Готово.", + failed: "Не удалось безопасно применить изменения.", + }; + const text = stageToText[ev.payload.stage] ?? ev.payload.message; + setMessages((m) => [...m, { role: "system", text: ev.payload.attempt > 0 ? `Попытка ${ev.payload.attempt}/2. ${text}` : text }]); + }); + return () => { + unlisten.then((fn) => fn()); + }; + }, []); + + useEffect(() => { + const unlisten = listen("analyze_progress", (ev) => { + if (applyingRef.current && typeof ev.payload === "string") { + setApplyProgressLog((prev) => [...prev, ev.payload]); + } + }); + return () => { + unlisten.then((fn) => fn()); + }; + }, []); + + const syncFolderLinksToBackend = async (paths: string[]) => { + try { + await setFolderLinksBackend(paths); + } catch (_) {} + }; + + const addFolder = async () => { + const selected = await open({ directory: true, multiple: false }); + if (selected && typeof selected === "string") { + const next = [...folderLinks, selected]; + setFolderLinks(next); + saveLocalLinks(next); + syncFolderLinksToBackend(next); + setLastPath(selected); + try { + const p = await getProjectProfile(selected); + setProfile(p); + setMessages((m) => [...m, { role: "system", text: `Профиль: ${p.project_type} · Safe Mode · Attempts: ${p.max_attempts}` }]); + } catch (_) { + setProfile(null); + } + } + }; + + const removeLink = (idx: number) => { + const next = folderLinks.filter((_, i) => i !== idx); + setFolderLinks(next); + saveLocalLinks(next); + syncFolderLinksToBackend(next); + }; + + const ALLOWED_FILE_EXT = new Set( + ["ts", "tsx", "js", "jsx", "rs", "py", "json", "toml", "md", "yml", "yaml", "css", "html", "xml"].map((e) => e.toLowerCase()) + ); + + const addFile = async () => { + const selected = await open({ + directory: false, + multiple: true, + title: "Выберите файлы (исходники, конфиги)", + // Без filters — на macOS диалог показывает все файлы; разрешённые форматы отбираем ниже. + }); + if (selected) { + const paths = Array.isArray(selected) ? selected : [selected]; + const valid = paths + .filter((p): p is string => typeof p === "string" && p.trim().length > 0) + .filter((p) => { + const ext = p.split(/[/\\]/).pop()?.split(".").pop()?.toLowerCase() ?? ""; + return ext && ALLOWED_FILE_EXT.has(ext); + }); + if (valid.length) setAttachedFiles((prev) => [...prev, ...valid]); + } + }; + + const removeFile = (idx: number) => { + setAttachedFiles((prev) => prev.filter((_, i) => i !== idx)); + }; + void [addFolder, removeLink, addFile, removeFile]; // Reserved for PathSelector UI + + /** Считаем ввод путём, только если он похож на путь к папке/файлу (иначе это вопрос — не анализировать как путь). */ + const inputLooksLikePath = (t: string): boolean => { + if (!t || t.length > 260) return false; + if (/^[/~.]/.test(t) || /^[A-Za-z]:[/\\]/.test(t)) return true; + if (/[/\\]/.test(t)) return true; + if (!/\s/.test(t) && t.length < 80) return true; + return false; + }; + + const pathsToUse = (): string[] => { + const t = input.trim(); + if (t && inputLooksLikePath(t)) return [t]; + const folders = folderLinks.length ? folderLinks : []; + const fileParentDirs = attachedFiles + .map((f) => f.replace(/[/\\][^/\\]+$/, "")) + .filter((d) => d && !folders.includes(d)); + const uniq = [...new Set([...folders, ...fileParentDirs])]; + return uniq.length ? uniq : ["."]; + }; + + const runBatch = async (confirmApply: boolean, actionsToApply: Action[], pathsOverride?: string[], userConfirmed?: boolean) => { + const paths = pathsOverride ?? pathsToUse(); + const usedInputAsPath = !!input.trim() && inputLooksLikePath(input.trim()); + setLoading(true); + if (!pathsOverride) { + if (input.trim() && !usedInputAsPath) { + setMessages((m) => [ + ...m, + { role: "system", text: "Это похоже на вопрос, а не на путь. Для анализа введите путь к проекту в поле ввода (например ./papa-yu или полный путь к папке)." }, + ]); + } + setMessages((m) => [...m, { role: "user", text: paths.length ? `Анализ: ${paths.join(", ")}` : "Анализ проекта" }]); + } + else setMessages((m) => [...m, { role: "user", text: "Применить изменения" }]); + if (confirmApply) setPendingPreview(null); + try { + if (confirmApply) setMessages((m) => [...m, { role: "system", text: "Применяю изменения пакетом…" }]); + const events = await runBatchCmd({ + paths, + confirm_apply: confirmApply, + auto_check: autoCheck, + selected_actions: actionsToApply.length ? actionsToApply : undefined, + user_confirmed: userConfirmed ?? confirmApply, + attached_files: attachedFiles.length ? attachedFiles : undefined, + }); + for (const ev of events) { + if (ev.kind === "report" && ev.report) { + setLastReport(ev.report); + setLastPath(ev.report.path); + setLastReportJson(JSON.stringify(ev.report, null, 2)); + try { + const p = await getProjectProfile(ev.report.path); + setProfile(p); + } catch (_) { + setProfile(null); + } + setSelectedActions(ev.report.actions || []); + setSuggestedActions([]); + setSelectedActionIdx({}); + const groups = ev.report.action_groups ?? []; + const initial: Record = {}; + for (const g of groups) initial[g.id] = true; + setSelectedFixGroupIds(initial); + const packs = ev.report.fix_packs ?? []; + const rec = ev.report.recommended_pack_ids ?? []; + const packInit: Record = {}; + for (const p of packs) packInit[p.id] = rec.includes(p.id); + setSelectedPackIds(packInit); + const x = ev.report.findings?.length ?? 0; + const y = (ev.report.actions?.length ?? 0); + setMessages((m) => [ + ...m, + { + role: "assistant", + text: `Нашёл ${x} проблем. Могу исправить ${y}.`, + report: ev.report, + }, + ]); + } else if (ev.kind === "preview" && ev.preview) { + setPendingPreview({ + path: lastPath || paths[0] || ".", + actions: actionsToApply.length ? actionsToApply : lastReport?.actions || [], + diffs: ev.preview.diffs, + }); + setMessages((m) => [ + ...m, + { + role: "assistant", + text: `Предпросмотр: ${ev.preview!.summary}`, + preview: ev.preview, + }, + ]); + if (ev.preview.diffs.some((d) => (d.summary || "").includes("BLOCKED"))) { + setMessages((m) => [...m, { role: "system", text: "Некоторые изменения заблокированы политикой (защищённые/не-текстовые файлы)." }]); + } + } else if (ev.kind === "apply" && ev.apply_result) { + const r = ev.apply_result; + setPendingPreview(null); + if (ev.undo_available !== undefined) setUndoAvailable(!!ev.undo_available); + const isAutoRollback = r.error_code === "AUTO_ROLLBACK_DONE"; + const isReverted = r.error_code === "AUTO_CHECK_FAILED_REVERTED" || r.error_code === "AUTO_CHECK_FAILED_ROLLED_BACK"; + if (isAutoRollback) { + setMessages((m) => [ + ...m, + { role: "system", text: "Обнаружены ошибки. Откатываю изменения…", applyResult: r }, + { role: "system", text: "Изменения привели к ошибкам, откат выполнен." }, + { role: "assistant", text: "Изменения привели к ошибкам, откат выполнен." }, + ]); + } else { + const code = r.error_code || ""; + let systemText = isReverted ? "Ошибки после изменений. Откат выполнен." : (r.error || (r.ok ? "Применено." : "Ошибка.")); + if (code === "CONFIRM_REQUIRED") systemText = "Подтверждение обязательно перед применением."; + else if (code === "PROTECTED_PATH") systemText = "Изменения отклонены: попытка изменить защищённые/не-текстовые файлы."; + setMessages((m) => [ + ...m, + { role: "system", text: systemText, applyResult: r }, + { + role: "assistant", + text: r.ok + ? "Изменения применены. Проверьте проект (тесты/сборка)." + : (isReverted ? "Ошибки после изменений. Откат выполнен." : code === "CONFIRM_REQUIRED" ? "Подтверждение обязательно перед применением." : code === "PROTECTED_PATH" ? "Изменения отклонены: защищённые/не-текстовые файлы." : r.error || "Ошибка применения."), + }, + ]); + } + refreshUndoRedo(); + } + } + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + } finally { + setLoading(false); + } + }; + + /** Отправить: если ввод — команда/задача (не путь), в первую очередь выполнить её через ИИ; иначе — анализ. */ + const onAnalyze = () => { + const text = input.trim(); + if (!text) { + runBatch(false, []); + return; + } + if (inputLooksLikePath(text)) { + runBatch(false, []); + return; + } + const pathToUse = lastPath || folderLinks[0] || pathsToUse()[0]; + if (pathToUse && pathToUse !== ".") { + setAgentGoal(text); + setInput(""); + if (!lastPath) setLastPath(pathToUse); + handleProposeFixes(text, pathToUse, lastReportJson ?? "{}"); + return; + } + if (pathToUse === ".") { + setAgentGoal(text); + setInput(""); + handleProposeFixes(text, ".", lastReportJson ?? "{}"); + return; + } + setMessages((m) => [ + ...m, + { role: "system", text: "Укажите папку проекта (скрепка → Папки или введите путь) и повторите команду." }, + ]); + runBatch(false, []); + }; + + const onShowFixes = () => { + if (!lastReport?.actions?.length) return; + setSelectedActions([...lastReport.actions]); + runBatch(false, lastReport.actions); + }; + + /** v2.9.1: один клик — применить все рекомендованные исправления из отчёта (confirm=true, auto_check=true) */ + const onApplyFixes = () => { + if (!lastReport?.actions?.length || !lastReport?.path) return; + setSelectedActions([...lastReport.actions]); + runBatch(true, lastReport.actions, [lastReport.path]); + }; + + function collectSelectedActions(report: AnalyzeReport | null, selected: Record): Action[] { + const groups = report?.action_groups ?? []; + const actions: Action[] = []; + for (const g of groups) { + if (selected[g.id]) actions.push(...(g.actions ?? [])); + } + return actions; + } + + function collectGroupIdsFromPacks(report: AnalyzeReport | null, selected: Record): string[] { + const packs = report?.fix_packs ?? []; + const ids = new Set(); + for (const p of packs) { + if (selected[p.id]) for (const gid of p.group_ids ?? []) ids.add(gid); + } + return Array.from(ids); + } + + function collectActionsByGroupIds(report: AnalyzeReport | null, groupIds: string[]): Action[] { + const groups = report?.action_groups ?? []; + const map = new Map(); + for (const g of groups) map.set(g.id, g); + const actions: Action[] = []; + for (const id of groupIds) { + const g = map.get(id); + if (g?.actions?.length) actions.push(...g.actions); + } + return actions; + } + + const handlePreview = (path: string | null, actions: Action[]) => { + if (!actions.length) return; + const p = path ? [path] : pathsToUse(); + runBatch(false, actions, p); + }; + + /** v3.1: применить через apply_actions_tx (snapshot + autocheck + rollback) */ + const applyActionsTx = async (path: string, actions: Action[], useAutoCheck = true) => { + setLoading(true); + try { + const res = await apiApplyActionsTx(path, actions, { + auto_check: useAutoCheck, + user_confirmed: true, + }); + if (res.ok) { + setMessages((m) => [...m, { role: "system", text: "Изменения применены. Проверки пройдены." }]); + setPendingPreview(null); + await refreshUndoRedo(); + } else { + const code = res.error_code || ""; + if (code === "CONFIRM_REQUIRED") { + setMessages((m) => [...m, { role: "system", text: "Подтверждение обязательно перед применением." }]); + } else if (code === "PROTECTED_PATH") { + setMessages((m) => [...m, { role: "system", text: "Изменения отклонены: попытка изменить защищённые/не-текстовые файлы." }]); + } else if (code === "AUTO_CHECK_FAILED_ROLLED_BACK") { + setMessages((m) => [...m, { role: "system", text: "Изменения привели к ошибкам, откат выполнен." }]); + } else { + setMessages((m) => [...m, { role: "system", text: res.error || res.error_code || "Ошибка применения." }]); + } + } + return res; + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + return { ok: false, applied: false, rolled_back: false, checks: [] } as ApplyTxResult; + } finally { + setLoading(false); + } + }; + + /** v3.1: предпросмотр + применить всё безопасное (с autocheck) */ + const applyAllSafe = async (projectPath: string, actions: Action[]) => { + setMessages((m) => [...m, { role: "system", text: "Предпросмотр изменений…" }]); + await handlePreview(projectPath, actions); + setMessages((m) => [...m, { role: "system", text: "Применяю изменения…" }]); + await applyActionsTx(projectPath, actions, true); + }; + + /** Применить изменения с отображением процесса в диалоге */ + const applyWithProgressDialog = async (path: string, actions: Action[]) => { + setApplyProgressVisible(true); + setApplyProgressLog(["Подготовка…"]); + setApplyResult(null); + applyingRef.current = true; + try { + const res = await apiApplyActionsTx(path, actions, { + auto_check: autoCheck, + user_confirmed: true, + }); + setApplyResult(res); + setApplyProgressLog((prev) => [ + ...prev, + res.ok ? "Готово. Изменения применены." : (res.error || "Ошибка"), + ]); + if (res.ok) { + setMessages((m) => [...m, { role: "system", text: "Изменения применены. Проверки пройдены." }]); + setPendingPreview(null); + setPendingActions(null); + setPendingActionIdx({}); + await refreshUndoRedo(); + } else { + const code = res.error_code || ""; + if (code === "CONFIRM_REQUIRED") { + setMessages((m) => [...m, { role: "system", text: "Подтверждение обязательно перед применением." }]); + } else if (code === "AUTO_CHECK_FAILED_ROLLED_BACK") { + setMessages((m) => [...m, { role: "system", text: "Изменения привели к ошибкам, откат выполнен." }]); + } else { + setMessages((m) => [...m, { role: "system", text: res.error || res.error_code || "Ошибка применения." }]); + } + } + } catch (e) { + const err = String(e); + setApplyProgressLog((prev) => [...prev, `Ошибка: ${err}`]); + setApplyResult({ ok: false, applied: false, rolled_back: false, checks: [], error: err } as ApplyTxResult); + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${err}` }]); + } finally { + applyingRef.current = false; + } + }; + + const handleApplyFixesWithActions = (path: string | null, actions: Action[]) => { + if (!actions.length) return; + if (path) { + const ok = window.confirm(`Применить ${actions.length} изменений к проекту?`); + if (!ok) return; + applyWithProgressDialog(path, actions); + return; + } + const p = pathsToUse(); + runBatch(true, actions, p); + }; + + const onApplyPending = () => { + if (!pendingPreview) return; + const ok = window.confirm("Применить изменения к проекту? Это изменит файлы на диске."); + if (!ok) return; + if (lastPath) { + applyWithProgressDialog(lastPath, pendingPreview.actions); + return; + } + const paths = pathsToUse(); + runBatch(true, pendingPreview.actions, paths, true); + }; + + const onCancelPending = () => { + setPendingPreview(null); + setMessages((m) => [...m, { role: "system", text: "Предпросмотр отменён. Ничего не изменено. Можно отправить новый запрос — введите путь или цель и нажмите «Отправить»." }]); + }; + + /** Сохранить текущий диалог в историю и переключиться на новый запрос. Контекст старого запроса сбрасывается, чтобы следующий выполнялся как новый. */ + const onNewRequest = () => { + if (messages.length > 0) { + const title = messages.find((m) => m.role === "user")?.text?.slice(0, 45) || "Запрос"; + setRequestHistory((prev) => [ + ...prev, + { id: String(Date.now()), title: title + (title.length >= 45 ? "…" : ""), messages: [...messages], lastPath, lastReport }, + ]); + } + setInput(""); + setAgentGoal(""); + setLastReport(null); + setLastReportJson(null); + setPendingPreview(null); + setPendingActions(null); + setPendingActionIdx({}); + setAgenticResult(null); + setAgenticProgress(null); + setVerifyResult(null); + setMessages((m) => [...m, { role: "system", text: "Готов к новому запросу. Введите путь или задачу и нажмите «Отправить»." }]); + }; + + /** Вернуться к обсуждению выбранного запроса из истории. */ + const switchToRequest = (item: { id: string; title: string; messages: ChatMessage[]; lastPath: string | null; lastReport: AnalyzeReport | null }) => { + setMessages(item.messages); + setLastPath(item.lastPath); + setLastReport(item.lastReport); + setPendingPreview(null); + setPendingActions(null); + setPendingActionIdx({}); + }; + + /** Удалить чат из истории. */ + const removeFromHistory = (id: string) => { + setRequestHistory((prev) => prev.filter((item) => item.id !== id)); + }; + + /** Список для отображения: текущий запрос (если есть сообщения) + история. */ + const displayRequests: { id: string; title: string; isCurrent?: boolean; item?: typeof requestHistory[0] }[] = []; + if (messages.length > 0) { + const currentTitle = messages.find((m) => m.role === "user")?.text?.slice(0, 45) || "Текущий запрос"; + displayRequests.push({ id: "current", title: currentTitle + (currentTitle.length >= 45 ? "…" : ""), isCurrent: true }); + } + requestHistory.forEach((item) => displayRequests.push({ id: item.id, title: item.title, item })); + + /** v3.3: один клик — generate → preview → apply (без показа списка) */ + const handleOneClickFix = async () => { + if (!lastPath || !lastReport) return; + setLoading(true); + setMessages((m) => [...m, { role: "system", text: "Формирую безопасные исправления…" }]); + try { + const res = await generateActionsFromReport( + lastPath, + lastReport, + createOnlyMode ? "safe_create_only" : "safe" + ); + if (!res.ok || res.actions.length === 0) { + setMessages((m) => [...m, { role: "assistant", text: res.error ?? res.actions.length === 0 ? "Нет безопасных правок." : "Ошибка генерации." }]); + return; + } + setSuggestedActions(res.actions); + const allSelected: Record = {}; + res.actions.forEach((_, i) => { allSelected[i] = true; }); + setSelectedActionIdx(allSelected); + setMessages((m) => [...m, { role: "assistant", text: "Предпросмотр изменений" }]); + await handlePreview(lastPath, res.actions); + setMessages((m) => [...m, { role: "system", text: "Применяю…" }]); + const applyRes = await applyActionsTx(lastPath, res.actions, true); + if (applyRes.ok) { + setMessages((m) => [...m, { role: "assistant", text: "Готово. Проверки пройдены." }]); + } else if (applyRes.error_code === "AUTO_CHECK_FAILED_ROLLED_BACK") { + setMessages((m) => [...m, { role: "assistant", text: "Откат выполнен." }]); + } else { + setMessages((m) => [...m, { role: "assistant", text: applyRes.error ?? "Ошибка применения." }]); + } + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + } finally { + setLoading(false); + } + }; + + /** v2.4: Agentic Run — analyze → plan → preview → apply → verify → auto-rollback → retry */ + const handleAgenticRun = async () => { + if (!lastPath) return; + setAgenticRunning(true); + setAgenticResult(null); + setAgenticProgress(null); + setMessages((m) => [...m, { role: "user", text: "Исправить проект автоматически" }]); + try { + const payload: AgenticRunRequest = { + path: lastPath, + goal: "Исправь критические проблемы и улучшай качество проекта", + constraints: { auto_check: true, max_attempts: 2, max_actions: 12 }, + }; + const result = await agenticRun(payload); + setAgenticResult(result); + setMessages((m) => [ + ...m, + { role: "assistant", text: result.final_summary }, + ]); + try { + const projects = await listProjects(); + let projectId = projects.find((p) => p.path === lastPath)?.id; + if (!projectId) { + const added = await addProject(lastPath, null); + projectId = added.id; + } + await appendSessionEvent(projectId, "agentic_run", "assistant", result.final_summary); + const list = await listSessions(projectId); + setSessions(list); + } catch (_) {} + await refreshUndoRedo(); + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + } finally { + setAgenticRunning(false); + setAgenticProgress(null); + } + }; + + /** v3.2: исправить автоматически (безопасно) — generate_actions_from_report → список с чекбоксами */ + const handleFixAuto = async () => { + if (!lastPath || !lastReport) return; + setIsGeneratingActions(true); + setMessages((m) => [...m, { role: "system", text: "Формирую безопасные исправления…" }]); + try { + const res = await generateActionsFromReport( + lastPath, + lastReport, + createOnlyMode ? "safe_create_only" : "safe" + ); + if (!res.ok) { + setMessages((m) => [...m, { role: "assistant", text: res.error ?? res.error_code ?? "Ошибка генерации" }]); + return; + } + setSuggestedActions(res.actions); + const allSelected: Record = {}; + res.actions.forEach((_, i) => { allSelected[i] = true; }); + setSelectedActionIdx(allSelected); + const summary = res.actions.length + ? `Предложено ${res.actions.length} действий. Выберите и примените.` + : "Нет безопасных правок для автоматического применения."; + if (res.skipped.length) { + setMessages((m) => [...m, { role: "assistant", text: `${summary} Пропущено: ${res.skipped.join(", ")}` }]); + } else { + setMessages((m) => [...m, { role: "assistant", text: summary }]); + } + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + } finally { + setIsGeneratingActions(false); + } + }; + + /** Выбранные действия из suggestedActions по selectedActionIdx */ + const getSelectedSuggestedActions = (): Action[] => + suggestedActions.filter((_, i) => selectedActionIdx[i] !== false); + + /** Выбранные рекомендации ИИ из pendingActions по pendingActionIdx */ + const getSelectedPendingActions = (): Action[] => + (pendingActions ?? []).filter((_, i) => pendingActionIdx[i] !== false); + + /** Собрать контекст трендов для ИИ: ИИ использует его самостоятельно при предложениях. */ + const getTrendsContextForAI = (): string | undefined => { + if (!trends?.recommendations?.length) return undefined; + return trends.recommendations + .map((r) => `• ${r.title}${r.summary ? `: ${r.summary}` : ""}`) + .join("\n"); + }; + + /** v3.0: предложить исправления (агент) → план по цели. ИИ в первую очередь выполняет команду пользователя. path и reportJson можно передать явно (при вводе команды без предварительного анализа). */ + const handleProposeFixes = async (overrideGoal?: string, overridePath?: string, overrideReportJson?: string) => { + const pathToUse = overridePath ?? lastPath; + const reportToUse = overrideReportJson ?? lastReportJson ?? "{}"; + if (!pathToUse) return; + const goal = (overrideGoal ?? agentGoal).trim() || "Повысить качество проекта и привести структуру к стандарту"; + if (goal) setMessages((m) => [...m, { role: "user", text: goal }]); + setMessages((m) => [...m, { role: "system", text: "Выполняю команду…" }]); + setLoading(true); + try { + let trendsContext = getTrendsContextForAI(); + if (!trendsContext && !trends) { + try { + const t = await getTrendsRecommendations(); + setTrends(t); + trendsContext = t.recommendations?.length + ? t.recommendations.map((r) => `• ${r.title}${r.summary ? `: ${r.summary}` : ""}`).join("\n") + : undefined; + } catch (_) {} + } + const plan = await proposeActions( + pathToUse, + reportToUse, + goal, + designStyle.trim() || undefined, + trendsContext, + lastPlanJson ?? undefined, + lastPlanContext ?? undefined + ); + if (!plan.ok) { + setMessages((m) => [...m, { role: "assistant", text: plan.error ?? "Ошибка формирования плана" }]); + return; + } + // Сохраняем план и контекст для Apply (когда пользователь напишет "ok" или "применяй") + if (plan.plan_json) { + setLastPlanJson(plan.plan_json); + setLastPlanContext(plan.plan_context ?? null); + } else { + setLastPlanJson(null); + setLastPlanContext(null); + } + const actionLines = plan.actions.length + ? "\n\nПлан действий:\n" + plan.actions.map((a) => `• ${a.kind}: ${a.path}`).join("\n") + : ""; + setMessages((m) => [...m, { role: "assistant", text: plan.summary + actionLines }]); + if (plan.actions.length > 0) { + setPendingActions(plan.actions); + try { + const preview = await previewActions(pathToUse, plan.actions); + setPendingPreview({ path: pathToUse, actions: plan.actions, diffs: preview.diffs }); + } catch (_) { + setPendingPreview(null); + } + } else { + setPendingActions(null); + setPendingPreview(null); + } + setPendingActionIdx({}); + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка: ${String(e)}` }]); + } finally { + setLoading(false); + } + }; + + const hasPendingPreview = !!pendingPreview; + + const handleDownloadReport = () => { + if (!lastReport) return; + const blob = new Blob([JSON.stringify(lastReport, null, 2)], { type: "application/json" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "report.json"; + a.click(); + URL.revokeObjectURL(a.href); + }; + + /** Обновить тренды и рекомендации (мониторинг не реже раз в месяц). */ + const handleFetchTrends = async () => { + setTrendsLoading(true); + try { + const res = await fetchTrendsRecommendations(); + setTrends(res); + } catch (_) { + setTrends(null); + } finally { + setTrendsLoading(false); + } + }; + + /** Проверка целостности проекта (типы, сборка, тесты). Вызывается автоматически после применений или вручную. */ + const handleVerifyIntegrity = async () => { + if (!lastPath) return; + setVerifying(true); + setVerifyResult(null); + try { + const res = await verifyProject(lastPath); + setVerifyResult(res); + const msg = res.ok + ? `Проверка целостности: всё в порядке (${res.checks.length} проверок).` + : `Проверка целостности: обнаружены ошибки. ${res.error ?? ""}`; + setMessages((m) => [...m, { role: "system", text: msg }]); + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка проверки: ${String(e)}` }]); + } finally { + setVerifying(false); + } + }; + + const handleDownloadDiff = () => { + if (!agenticResult?.attempts?.length) return; + const last = agenticResult.attempts[agenticResult.attempts.length - 1]; + if (!last?.preview?.diffs?.length) return; + const lines = last.preview.diffs.map((d) => + `--- ${d.path}\n+++ ${d.path}\n${(d.old_content ?? "") ? `- ${(d.old_content ?? "").split("\n").join("\n- ")}\n` : ""}${(d.new_content ?? "") ? `+ ${(d.new_content ?? "").split("\n").join("\n+ ")}` : ""}` + ); + const blob = new Blob([lines.join("\n\n")], { type: "text/plain" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "changes.diff"; + a.click(); + URL.revokeObjectURL(a.href); + }; + + const handleExportSettings = async () => { + try { + const json = await exportSettings(); + const blob = new Blob([json], { type: "application/json" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `papa-yu-settings-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(a.href); + setMessages((m) => [...m, { role: "system", text: "Настройки экспортированы в файл." }]); + } catch (e) { + setMessages((m) => [...m, { role: "system", text: `Ошибка экспорта: ${e}` }]); + } + }; + + const handleImportSettings = () => { + // Create hidden file input + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json,application/json"; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + try { + const json = await file.text(); + const result = await importSettings(json, "merge"); + setMessages((m) => [ + ...m, + { + role: "system", + text: `Импортировано: ${result.projects_imported} проектов, ${result.profiles_imported} профилей, ${result.sessions_imported} сессий, ${result.folder_links_imported} папок.`, + }, + ]); + // Reload folder links + const links = await getFolderLinks(); + if (links.paths?.length) setFolderLinks(links.paths); + } catch (err) { + setMessages((m) => [...m, { role: "system", text: `Ошибка импорта: ${err}` }]); + } + }; + input.click(); + }; + + return ( +
+ {/* Левая панель: запросы и кнопки */} + + +
+
+ PAPAYU +
+

PAPA YU

+

инженерная система с контролем качества и эксплуатационными инструментами

+
+
+ + {profile && ( +
+ Профиль: {profile.project_type} · Safe Mode · Attempts: {profile.max_attempts} + {profile.limits && ( + <> · Лимиты: {profile.limits.max_actions_per_tx} действий/транзакция, таймаут {profile.limits.timeout_sec} с + )} +
+ )} + {lastPath && ( +
+ + + Автоматическая проверка типов, сборки и тестов после изменений + +
+ )} + {verifyResult && ( +
+

+ {verifyResult.ok ? "Целостность в порядке" : "Обнаружены ошибки"} +

+
    + {verifyResult.checks.map((c, i) => ( +
  • + {c.ok ? "✓" : "✗"} {c.name} + {!c.ok && c.output && ( +
    {c.output}
    + )} +
  • + ))} +
+
+ )} + {/* Инлайн-запрос к ИИ: постановка задачи и ответ с вариантами в этом же окне */} + {lastPath && lastReport && ( +
+

+ Запрос к ИИ +

+

+ Опишите задачу. При создании программ можно выбрать стиль дизайна (ИИ или сторонние: Material, Tailwind/shadcn, Bootstrap). Ответ и варианты появятся ниже. +

+
+ + +
+
+