diff --git a/.github/workflows/album-deduplicator-windows-release.yml b/.github/workflows/album-deduplicator-windows-release.yml new file mode 100644 index 0000000..1b65862 --- /dev/null +++ b/.github/workflows/album-deduplicator-windows-release.yml @@ -0,0 +1,172 @@ +name: Album Deduplicator Windows CI-CD + +on: + pull_request: + paths: + - "album_deduplicator/**" + - ".github/workflows/album-deduplicator-windows-release.yml" + push: + branches: + - main + tags: + - "album-deduplicator-v*" + paths: + - "album_deduplicator/**" + - ".github/workflows/album-deduplicator-windows-release.yml" + workflow_dispatch: + +concurrency: + group: album-deduplicator-windows-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.12" + NODE_VERSION: "24" + APP_DIR: album_deduplicator + FRONTEND_DIR: album_deduplicator/frontend + DESKTOP_DIST_DIR: album_deduplicator/frontend/desktop-dist + +jobs: + verify: + name: Verify Backend And Desktop Build Inputs + runs-on: windows-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: ${{ env.APP_DIR }}/requirements.txt + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: ${{ env.FRONTEND_DIR }}/package-lock.json + + - name: Install backend dependencies + run: python -m pip install --upgrade pip && python -m pip install -r "${{ env.APP_DIR }}/requirements.txt" + + - name: Install frontend dependencies + working-directory: ${{ env.FRONTEND_DIR }} + run: npm ci + + - name: Run backend tests + working-directory: ${{ env.APP_DIR }} + run: python -m pytest tests -q + + - name: Run frontend tests + working-directory: ${{ env.FRONTEND_DIR }} + run: npm test + + - name: Run renderer build + working-directory: ${{ env.FRONTEND_DIR }} + run: npm run build + + package-preview: + name: Build Unsigned Windows Installer Artifact + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' + needs: verify + runs-on: windows-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: ${{ env.APP_DIR }}/requirements.txt + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: ${{ env.FRONTEND_DIR }}/package-lock.json + + - name: Install backend dependencies + run: python -m pip install --upgrade pip && python -m pip install -r "${{ env.APP_DIR }}/requirements.txt" + + - name: Install frontend dependencies + working-directory: ${{ env.FRONTEND_DIR }} + run: npm ci + + - name: Package Windows installer + working-directory: ${{ env.FRONTEND_DIR }} + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: npm run dist:desktop + + - name: Upload Windows installer artifact + uses: actions/upload-artifact@v4 + with: + name: album-deduplicator-windows-preview-${{ github.run_number }} + path: | + ${{ env.DESKTOP_DIST_DIR }}/*.exe + ${{ env.DESKTOP_DIST_DIR }}/*.blockmap + ${{ env.DESKTOP_DIST_DIR }}/latest*.yml + if-no-files-found: error + retention-days: 14 + + publish-release: + name: Publish Windows Release To GitHub Releases + if: startsWith(github.ref, 'refs/tags/album-deduplicator-v') + needs: verify + runs-on: windows-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: ${{ env.APP_DIR }}/requirements.txt + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: ${{ env.FRONTEND_DIR }}/package-lock.json + + - name: Install backend dependencies + run: python -m pip install --upgrade pip && python -m pip install -r "${{ env.APP_DIR }}/requirements.txt" + + - name: Install frontend dependencies + working-directory: ${{ env.FRONTEND_DIR }} + run: npm ci + + - name: Publish Windows release assets + working-directory: ${{ env.FRONTEND_DIR }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: npm run dist:desktop:publish + + - name: Upload published release assets + uses: actions/upload-artifact@v4 + with: + name: album-deduplicator-windows-release-${{ github.ref_name }} + path: | + ${{ env.DESKTOP_DIST_DIR }}/*.exe + ${{ env.DESKTOP_DIST_DIR }}/*.blockmap + ${{ env.DESKTOP_DIST_DIR }}/latest*.yml + if-no-files-found: error + retention-days: 30 diff --git a/.gitignore b/.gitignore index ede99ba..6bd4c85 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,9 @@ cython_debug/ # pesonal o1_project/RecycleBin music_data.json +**/logs/** +**/repomix-output.* +.vscode/settings.json +**/node_modules/ +album_deduplicator/frontend/dist/ +album_deduplicator/frontend/desktop-dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6864773 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +## קווים מנחים +- כברירת מחדל, העבודה העיקרית שלך היא בתיקיה album_deduplicator בלבד! +- עליך לקרוא את הרידמי ואת הקבצים הרלוונטיים בתיקית album_deduplicator/docs +- עליך להקפיד על עיצוב בסגנון אפליקציית דסקטופ, ולא עיצוב web! +- במשימת עיצוב, תיעזר ב-skill ant-design +- כאשר אתה מבצע משימות מקובץ ISSUE.md או קבצים אחרים, עדכן בדרך כלל את album_deduplicator/CHANGELOG.md לאחר היישום, בקצרה וללא רישום שינויים קטנים. עדכן קבצי תיעוד אחרים רק כאשר השינוי משנה ארכיטקטורה, תהליך עבודה, API, או התנהגות שדורשת הסבר מתמשך. +- אל תיצור קבצי ענק, בצע חלוקה לרכיבים אם קבצים הופכים לגדולים מידי +- צור קומיט לאחר סדרת שינויים שהושלמה בהצלחה. התעלם משינויים נפרדים diff --git a/Notes.txt b/Notes.txt deleted file mode 100644 index b301603..0000000 --- a/Notes.txt +++ /dev/null @@ -1,11 +0,0 @@ -## מצב נוכחי עבור ‏‏find_duplic_albums: - -המערכת בנויה יחסית טוב בצורה שניתן להשתמש בה דרך CMD. -בפועל, מיזוג התיקיות לא עובד כמצופה. הוא מוסיף מטאדאטה חסר אבל תמונת האלבום לא מתמזגת -וכן גם לא קיימת עדיפות למטדאטה בעברית על מטאדאטה באנגלית ועל אמיתית לעומת גנרית (כמו "artist" או "unknown artist") -ניתן לשלב את מודל ה-AI שלי לזיהוי שמות גנריים של אלבום או זמר - -ניתן בשלב ראשון לבצע מחיקה אוטומטית של תיקיות זהות ב-100% (עם האש) -ניתן להגדיר שתתבצע מחיקה של אחת התיקיות הזהות ללא הבחנה מיוחדת -או לחילופין - לבחור תיקיה ראשית עיקרית ותיקיה ראשית אחרת, כאשר תהיה עדיפות לשמור את התיקיה שנמצאת בתיקיה העיקרית -זה עשוי להיות שימושי גם עבור מיזוג תיקיות - למזג את התוכן לתיקיה שמשוייכת לתיקיה העיקרית \ No newline at end of file diff --git a/README.md b/README.md index 1624a63..9005756 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,55 @@ -# Music-Automatic - Streamlined sorting and reorganization of music folders on the computer through advanced automation - -**Plans to carry out**: -- Automatic conversion of corrupted metadata✅ -- Search and identify duplicate music folders and clean them -- Removing duplicate songs with smart detection -(in double singles - removal of the lower quality copy) -- Adding a name by title and vice versa, with a priority check -(names that contain the word "רצועה" instead of a song name for example) -- Overview with recommendations for implementation -- Search for video files that take up space -- Search for files with a high bit rate, with an option to shrink -- Converting files with an undefined name in English (for example: "track") to a corresponding name in Hebrew (such as: "רצועה")✅ +# Music-Automatic: אוטומציה לניהול ספריית המוזיקה שלך + +פרויקט זה נועד לספק כלים אוטומטיים לניהול, ארגון וניקוי של ספריות מוזיקה דיגיטליות גדולות. הוא מורכב ממספר מודולים, כל אחד עם התמחות ספציפית, הפועלים יחד כדי לשפר את חוויית ההאזנה שלך ולשמור על ספריית המוזיקה שלך מסודרת ויעילה. + +## 🌟 תכונות עיקריות + +* **ארגון קבצים ותיקיות:** כלים לתיקון שמות, מחיקת תיקיות ריקות ומיזוג תיקיות אמן. +* **זיהוי וטיפול בכפילויות:** מודול מתקדם לאיתור אלבומים כפולים או דומים מאוד, עם אפשרויות למיזוג מידע או מחיקה חכמה. +* **שיפור מטא-דאטה:** תיקון קידוד פגום (ג'יבריש), השלמת מידע חסר. +* **ניתוח איכות:** הערכת איכות אלבומים על בסיס פרמטרים מגוונים. +* **ממשקים גמישים:** אפשרות להפעלה דרך שורת הפקודה (CLI) או ממשק משתמש גרפי (Web UI). +* **שילוב למידת מכונה ו-AI:** שימוש במודל ML ייעודי וב-Gemini API לשיפור דיוק הזיהוי וההשוואה. + +## 📂 מבנה הפרויקט + +הפרויקט מחולק לשלוש ספריות עיקריות: + +### **`music_organizer`**: +מכיל סקריפטים לביצוע פעולות תחזוקה בסיסיות על קבצי מוזיקה ותיקיות. +**⚠️ אזהרה: ספרייה זו עדיין בפיתוח ואינה מוכנה לייצור. יש להשתמש בה בזהירות ולאחר גיבוי הנתונים.** +[למידע נוסף](./music_organizer/README.md) + +### **`album_deduplicator`**: +המודול המרכזי לזיהוי, ניתוח וטיפול באלבומי מוזיקה כפולים או דומים. כולל CLI, Web UI (מבוסס Streamlit), ומנוע השוואה מתוחכם. +[למידע נוסף](./album_deduplicator/README.md) + +### **`similarity_model`**: +מכיל את הקוד והנתונים הקשורים למודל למידת המכונה (LightGBM) המשמש לחיזוי דמיון בין אלבומים. כולל סקריפטים להכנת נתונים, אימון, הערכה ובדיקת המודל. +[למידע נוסף](./similarity_model/README.md) + +## 🚀 תוכנית עבודה + +* ✅ המרה אוטומטית של מטאדאטה פגום +* ✅ המרת קבצים עם שם לא מוגדר באנגלית (למשל: "track") לשם מתאים בעברית (כגון: "רצועה") +* ✅ חיפוש וזיהוי תיקיות מוזיקה כפולות וניקויין +* הסרת שירים כפולים עם זיהוי חכם (בסינגלים כפולים - הסרה של העותק הפחות איכותי) +* הוספת שם לפי כותרת ולהיפך, עם בדיקת עדיפות (שמות שמכילים את המילה "רצועה" במקום שם שיר למשל) +* סקירה כללית עם המלצות לביצוע +* חיפוש קבצי וידאו שתופסים מקום +* חיפוש קבצים בעלי קצב סיביות גבוה, עם אופציה לכיווץ + +## ⚙️ התקנה כללית + +דרישות ספציפיות לכל מודול מפורטות בקובצי ה-README המתאימים. באופן כללי, תזדקק ל-Python (מומלץ 3.10 ומעלה) ולמנהל חבילות כמו `pip`. + +```bash +git clone https://github.com/NHLOCAL/Music-Automatic.git +cd Music-Automatic +``` + +לאחר מכן, עקוב אחר הוראות ההתקנה בכל ספריית משנה. + +## 📄 רישיון + +פרויקט זה מופץ תחת רישיון MIT. ראה קובץ [`LICENSE`](LICENSE) לפרטים מלאים. diff --git a/album_deduplicator/.agents/skills/ant-design/LICENSE b/album_deduplicator/.agents/skills/ant-design/LICENSE new file mode 100644 index 0000000..9b563de --- /dev/null +++ b/album_deduplicator/.agents/skills/ant-design/LICENSE @@ -0,0 +1,22 @@ +MIT LICENSE + +Copyright (c) 2015-present Ant UED, https://xtech.antfin.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/album_deduplicator/.agents/skills/ant-design/SKILL.md b/album_deduplicator/.agents/skills/ant-design/SKILL.md new file mode 100644 index 0000000..b56ffeb --- /dev/null +++ b/album_deduplicator/.agents/skills/ant-design/SKILL.md @@ -0,0 +1,56 @@ +--- +name: ant-design +description: Single-file decision guide for antd 6.x, Ant Design Pro 5/ProComponents, and Ant Design X v2. Use for component selection, theming/tokens, SSR, a11y, performance, routing/access/CRUD, and AI/chat UI patterns. +--- + +# Ant Design + +## S - Scope +- Target: `antd@^6` + React 18-19, with `ant-design-pro@^5` / `@ant-design/pro-components` and `@ant-design/x@^2` when needed. +- Focus: decision guidance only; no end-user tutorials. +- Source policy: official docs only; no undocumented APIs or internal `.ant-*` coupling. + +### Default assumptions +- Language: TypeScript. +- Styling: tokens first, then `classNames`/`styles`; avoid global overrides. +- Provider: one root `ConfigProvider` unless strict isolation is required. + +### Mandatory rules +- For component questions, first map the component name to the official route slug `{components}` (lowercase kebab-case, e.g. `TreeSelect -> tree-select`, `Button -> button`), then request docs in this order (CN first, EN fallback): + 1. `https://ant.design/components/{components}-cn` + 2. `https://ant.design/components/{components}` + - Examples: `tree-select-cn -> tree-select`, `button-cn -> button`. +- Use only documented antd/Pro/X APIs. +- Do not invent props/events/component names. +- Do not rely on internal DOM or `.ant-*` selectors. +- Theme priority: global tokens -> component tokens -> alias tokens. + +## P - Process +### 1) Classify +- Identify layer: core antd, Pro, or X. +- Confirm version, rendering mode (CSR/SSR/streaming), and data scale. + +### 2) Request docs +- For each component, request `-cn.md` first, then `.md` fallback. +- If multiple components are involved, request each component page before deciding. + +### 3) Decide +- Provider baseline: CSR -> `ConfigProvider`; SSR -> `ConfigProvider` + `StyleProvider`. +- Theming baseline: global tokens -> component tokens -> `classNames`/`styles`. +- Output recommendation + risk + verification points (SSR/a11y/perf). + +## O - Output +- Provide short decision rationale (1-3 sentences). +- Include minimal provider/theming strategy. +- Include concrete SSR/a11y/perf checks. +- For Pro: include route/menu/access and CRUD schema direction. +- For X: include message/tool schema and streaming state direction. + +## Regression checklist +- [ ] One root `ConfigProvider`; SSR style order/hydration verified. +- [ ] Tokens first; no broad global `.ant-*` overrides. +- [ ] Table has stable `rowKey`; sort/filter/pagination entry is unified. +- [ ] Select remote mode disables local filter when using remote search. +- [ ] Upload controlled/uncontrolled mode is explicit with failure/retry path. +- [ ] Pro route/menu/access remain consistent with backend enforcement. +- [ ] X streaming supports stop/retry and deterministic tool rendering. diff --git a/album_deduplicator/.gitignore b/album_deduplicator/.gitignore new file mode 100644 index 0000000..0f92024 --- /dev/null +++ b/album_deduplicator/.gitignore @@ -0,0 +1,3 @@ +data/comparison_results_cache.json +data/** +repomix-output.* \ No newline at end of file diff --git a/album_deduplicator/CHANGELOG.md b/album_deduplicator/CHANGELOG.md new file mode 100644 index 0000000..17674b2 --- /dev/null +++ b/album_deduplicator/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +כל שינוי משמעותי ב־`album_deduplicator` יתועד בקובץ זה. + +הפורמט מבוסס על עקרונות `Keep a Changelog`, עם חלוקה לפי גרסאות או לפי `Unreleased` כאשר עדיין לא נוצר release רשמי. + +## Unreleased + +### Fixed + +- אריזת ה־desktop כוללת כעת את מודל ה־`ML`, כך שגרסאות Windows ארוזות לא נופלות ל־scoring אלגוריתמי בלבד. + +### Documentation + +- נוקה `README.md` מסעיף "מה חדש" ומרשימת עדכונים מצטברת. +- נוסף קובץ `CHANGELOG.md` שישמש מכאן ואילך לתיעוד שינויי קוד ועדכוני מוצר. + +### Current Baseline + +- היישום הראשי הוא desktop app מבוסס `Electron + React`, עם backend מקומי מבוסס `FastAPI`. +- ה־frontend משתמש ב־`Ant Design 6`, תמיכת RTL וזרימת עבודה למסכי בחירה, סריקה, סיכום, סקירה והעברה. +- מנוע הניתוח משלב scoring אלגוריתמי, מודל `ML` מקומי ו־`Gemini` אופציונלי לזוגות גבוליים. +- מחיקות מתבצעות דרך `send2trash` אל סל המחזור בלבד, לאחר אישור משתמש. +- קיימת תמיכה ב־sessions, אירועי `SSE`, שמירת החלטות משתמש, עטיפות אלבומים והשמעת שירים לצורך השוואה. +- קיימת תשתית CI/CD ל־Windows builds ול־GitHub Releases דרך tag מהצורה `album-deduplicator-vX.Y.Z`. diff --git a/album_deduplicator/README.md b/album_deduplicator/README.md new file mode 100644 index 0000000..16a6f68 --- /dev/null +++ b/album_deduplicator/README.md @@ -0,0 +1,125 @@ +# Album Deduplicator + +`Album Deduplicator` הוא מודול לזיהוי, סקירה וטיפול באלבומי מוזיקה כפולים או דומים. המערכת משלבת סריקת קבצים, השוואת מטא-דאטה, scoring אלגוריתמי, מודל `ML` מקומי וניתוח `Gemini` אופציונלי עבור מקרים גבוליים. + +היישום מיועד לעבודה כ־desktop app על `Windows`, עם ממשק `Electron + React` ו־backend מקומי מבוסס `FastAPI`. בנוסף קיימים entrypoints ל־`CLI` ולהרצת API עצמאית. + +## יכולות עיקריות + +- סריקת תיקיות מוזיקה וזיהוי אלבומים מועמדים להשוואה. +- קיבוץ אלבומים כפולים או דומים ל־clusters. +- המלצה על עותק לשמירה לפי איכות, העדפות root וספי ודאות. +- סקירה ידנית של הבדלים בין עותקים, כולל טבלאות שירים, עטיפות והשמעה ישירה. +- מחיקה בטוחה באמצעות `send2trash` אל סל המחזור בלבד. +- שמירת החלטות משתמש מקומיות וחיבור מחדש ל־session פעיל. +- תמיכה ב־`partial hash` כברירת מחדל וב־`full hash scan` לפי בחירה. + +## מבנה הפרויקט + +- `music_dup_lib/core/` - סריקה, חילוץ מטא-דאטה, hashing, השוואה וניתוח איכות. +- `music_dup_lib/services/` - orchestration, scoring, המלצות, מחיקה ושמירת החלטות. +- `music_dup_lib/external/` - אינטגרציות `ML` ו־`Gemini`. +- `api/` - שרת `FastAPI`, sessions, DTOs ו־`SSE`. +- `frontend/` - ממשק `React + Vite` ומעטפת `Electron`. +- `tests/` - בדיקות backend ו־API. +- `main.py` - ממשק `CLI`. + +## התקנה + +### Backend + +```bash +cd album_deduplicator +pip install -r requirements.txt +``` + +### Frontend / Desktop + +```bash +cd album_deduplicator/frontend +npm install +``` + +## הרצה + +### Desktop App + +```bash +cd album_deduplicator/frontend +npm run dev:electron +``` + +הפקודה מריצה את `Vite`, פותחת חלון `Electron`, מעלה backend מקומי של `FastAPI` ומחברת את ה־renderer לשרת המקומי. + +### API בלבד + +```bash +cd album_deduplicator +uvicorn api.app:app --reload +``` + +ברירת המחדל היא `http://127.0.0.1:8000`. + +### Frontend בלבד + +```bash +cd album_deduplicator/frontend +npm run dev +``` + +ברירת המחדל היא `http://127.0.0.1:5173`. + +### CLI + +```bash +cd album_deduplicator +python main.py "C:/Music" "D:/Archive" -p "C:/Music" +``` + +## בדיקות + +### Backend + +```bash +cd album_deduplicator +pytest tests -q +``` + +### Frontend + +```bash +cd album_deduplicator/frontend +npm test +npm run build +``` + +### Benchmark / Profiling + +```bash +cd album_deduplicator +python tools/benchmark_analysis.py "D:/שמע/כל המוזיקה" --report-path logs/benchmarks/latest.md +``` + +## אריזה ל־Windows + +```bash +cd album_deduplicator/frontend +npm run sync:icons +npm run dist:desktop +``` + +תהליך ה־CI/CD, convention של tags ומגבלות האריזה מתועדים ב־[docs/WINDOWS_RELEASES.md](docs/WINDOWS_RELEASES.md). + +## תיעוד נוסף + +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) - מבנה המערכת, שכבות אחריות וזרימת נתונים. +- [docs/ML_VALIDATION.md](docs/ML_VALIDATION.md) - ולידציית מודל ה־`ML` וספי `safe delete`. +- [docs/USER_FEEDBACK_DATASET.md](docs/USER_FEEDBACK_DATASET.md) - איסוף החלטות משתמשים לשיפור המודל. +- [CHANGELOG.md](CHANGELOG.md) - תיעוד שינויי קוד ועדכוני מוצר. + +## הערות תפעול + +- אם קובץ ה־`ML` לא נמצא תחת `album_deduplicator/data`, המערכת תחפש אותו גם תחת `similarity_model/models`. +- אם `Gemini` לא זמין, המערכת ממשיכה לעבוד ומסמנת degraded mode ב־API וב־UI. +- כל מחיקה מחייבת אישור משתמש ונשלחת אל סל המחזור בלבד. +- מעטפת ה־desktop משתמשת ב־`preload bridge` מאובטח לבחירת תיקיות ולפתיחת נתיבים ב־Explorer. diff --git a/album_deduplicator/api/__init__.py b/album_deduplicator/api/__init__.py new file mode 100644 index 0000000..6c61adf --- /dev/null +++ b/album_deduplicator/api/__init__.py @@ -0,0 +1,2 @@ +from .app import app, create_app + diff --git a/album_deduplicator/api/app.py b/album_deduplicator/api/app.py new file mode 100644 index 0000000..bcfe01b --- /dev/null +++ b/album_deduplicator/api/app.py @@ -0,0 +1,502 @@ +from __future__ import annotations + +import asyncio +import json +import os +import queue +from pathlib import Path +from typing import Literal + +from fastapi import FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, Response, StreamingResponse +from fastapi.staticfiles import StaticFiles +from mutagen._util import MutagenError +from mutagen.flac import FLAC +from mutagen.id3 import ID3, ID3NoHeaderError +from mutagen.mp3 import MP3, HeaderNotFoundError as MP3HeaderNotFoundError + +from music_dup_lib import config +from music_dup_lib.services import AnalysisOptions +from music_dup_lib.services.user_feedback_logger import build_feedback_export_filename +from api.schemas import ( + AnalysisSessionCreateRequest, + AnalysisSessionCreatedResponse, + ClusterListResponse, + ComparisonHighlightModel, + ClusterSummaryModel, + CountsResponse, + DecisionItem, + DecisionsRequest, + DeleteExecutionItemModel, + DeleteExecutionRequest, + DeleteExecutionResponse, + DeletePreviewItemModel, + DeletePreviewResponse, + DegradedFlags, + FeedbackSummaryResponse, + FolderSummaryModel, + GeminiSettingsResponse, + ModeSummary, + OpenExplorerRequest, + PairScoreBreakdownModel, + ProgressState, + RecommendationReasonModel, + SingleDeleteRequest, + SessionStatusResponse, + TrackInfoModel, +) +from api.session_store import SessionStore + + +store = SessionStore() + + +def _resolve_app_version() -> str: + env_version = os.getenv("ALBUM_DEDUP_VERSION", "").strip() + if env_version: + return env_version + + package_json_path = Path(__file__).resolve().parent.parent / "frontend" / "package.json" + try: + package_data = json.loads(package_json_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return "0.1.0" + + version = package_data.get("version") + return version.strip() if isinstance(version, str) and version.strip() else "0.1.0" + + +def create_app() -> FastAPI: + app = FastAPI(title="Album Deduplicator API", version=_resolve_app_version()) + app.add_middleware( + CORSMiddleware, + allow_origins=config.API_DEV_CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + frontend_dist = Path(__file__).resolve().parent.parent / "frontend" / "dist" + if frontend_dist.exists(): + app.mount("/assets", StaticFiles(directory=frontend_dist / "assets"), name="assets") + + @app.get("/", include_in_schema=False) + def serve_frontend() -> FileResponse: + return FileResponse(frontend_dist / "index.html") + + @app.get("/api/health") + def healthcheck() -> dict: + return {"status": "ok", "app": app.title, "version": app.version} + + @app.get("/api/settings/gemini", response_model=GeminiSettingsResponse) + def get_gemini_settings() -> GeminiSettingsResponse: + return GeminiSettingsResponse(has_api_key=store.gemini_settings_store.has_api_key()) + + @app.post("/api/analysis-sessions", response_model=AnalysisSessionCreatedResponse) + def create_analysis_session(payload: AnalysisSessionCreateRequest) -> AnalysisSessionCreatedResponse: + folders = [Path(folder).resolve() for folder in payload.folders] + invalid_paths = [str(folder) for folder in folders if not folder.is_dir()] + if not folders: + raise HTTPException(status_code=400, detail="At least one folder is required.") + if invalid_paths: + raise HTTPException(status_code=400, detail=f"Invalid folder paths: {', '.join(invalid_paths)}") + + preferred_roots = [Path(root).resolve() for root in (payload.preferred_roots or []) if root] + if not preferred_roots and payload.preferred_root and payload.use_preferred_roots: + preferred_roots = [Path(payload.preferred_root).resolve()] + invalid_preferred_roots = [root for root in preferred_roots if root not in folders] + if invalid_preferred_roots: + raise HTTPException(status_code=400, detail="preferred_roots must be selected from the input folders.") + preferred_root = preferred_roots[0] if payload.use_preferred_roots and preferred_roots else None + gemini_api_key = payload.gemini_api_key.strip() if payload.gemini_api_key else None + if gemini_api_key: + store.gemini_settings_store.save_api_key(gemini_api_key) + elif payload.gemini_enabled: + gemini_api_key = store.gemini_settings_store.get_api_key() + + session = store.create_session( + AnalysisOptions( + folders=folders, + preferred_root=preferred_root, + preferred_roots=preferred_roots, + use_preferred_roots=payload.use_preferred_roots, + bitrate_mode=payload.bitrate_mode, + force_rescan=payload.force_rescan, + clear_cache=payload.clear_cache, + gemini_enabled=payload.gemini_enabled, + gemini_api_key=gemini_api_key, + full_hash_scan=payload.full_hash_scan, + ) + ) + store.start_analysis(session.session_id) + return AnalysisSessionCreatedResponse(session_id=session.session_id, status="queued") + + @app.get("/api/analysis-sessions/{session_id}", response_model=SessionStatusResponse) + def get_analysis_session(session_id: str) -> SessionStatusResponse: + session = _get_session_or_404(session_id) + return SessionStatusResponse( + session_id=session.session_id, + status=session.status, + progress=ProgressState(**session.progress), + mode_summary=ModeSummary( + ml_default_enabled=True, + gemini_enabled=session.options.gemini_enabled, + review_threshold=config.REVIEW_MIN_SIMILARITY, + safe_delete_threshold=config.SAFE_DELETE_MIN_SIMILARITY, + ), + degraded_flags=DegradedFlags( + ml_unavailable=session.snapshot.warnings.ml_unavailable, + gemini_unavailable=session.snapshot.warnings.gemini_unavailable, + warnings=session.snapshot.warnings.warnings, + ), + counts=CountsResponse( + folders=session.snapshot.counts.folders, + compared_pairs=session.snapshot.counts.compared_pairs, + safe_clusters=session.snapshot.counts.safe_clusters, + review_clusters=session.snapshot.counts.review_clusters, + ), + error=session.error, + ) + + @app.get("/api/analysis-sessions/{session_id}/events") + async def stream_analysis_events(session_id: str) -> StreamingResponse: + session = _get_session_or_404(session_id) + + async def event_generator(): + while True: + try: + event = session.events.get_nowait() + payload = json.dumps(event["data"], ensure_ascii=False) + yield f"event: {event['event']}\ndata: {payload}\n\n" + except queue.Empty: + if session.status in {"completed", "failed"} and session.events.empty(): + final_payload = json.dumps({"status": session.status}, ensure_ascii=False) + yield f"event: end\ndata: {final_payload}\n\n" + break + await asyncio.sleep(0.25) + + return StreamingResponse(event_generator(), media_type="text/event-stream") + + @app.get("/api/analysis-sessions/{session_id}/clusters", response_model=ClusterListResponse) + def get_clusters( + session_id: str, + bucket: Literal["safe", "review", "all"] = Query(default="all"), + ) -> ClusterListResponse: + session = _get_session_or_404(session_id) + clusters = [ + _cluster_model(session, cluster_id) + for cluster_id in session.snapshot.clusters + if bucket == "all" or session.snapshot.clusters[cluster_id].confidence_bucket == bucket + ] + return ClusterListResponse(clusters=clusters) + + @app.post("/api/analysis-sessions/{session_id}/decisions", response_model=DeletePreviewResponse) + def save_decisions(session_id: str, payload: DecisionsRequest) -> DeletePreviewResponse: + _get_session_or_404(session_id) + preview = store.apply_decisions( + session_id, + {decision.cluster_id: decision.keeper_id for decision in payload.decisions}, + { + decision.cluster_id: set(decision.delete_folder_ids or []) + for decision in payload.decisions + if decision.delete_folder_ids is not None + }, + ) + return _preview_model(preview) + + @app.get("/api/analysis-sessions/{session_id}/delete-preview", response_model=DeletePreviewResponse) + def get_delete_preview(session_id: str) -> DeletePreviewResponse: + _get_session_or_404(session_id) + preview = store.get_preview(session_id) + return _preview_model(preview) + + @app.get("/api/ml-feedback/summary", response_model=FeedbackSummaryResponse) + def get_feedback_summary() -> FeedbackSummaryResponse: + summary = store.feedback_logger.summary() + return FeedbackSummaryResponse( + feedback_file_path=str(summary.feedback_file_path), + event_count=summary.event_count, + size_bytes=summary.size_bytes, + export_url=summary.export_url, + ) + + @app.get("/api/ml-feedback/export") + def export_feedback() -> FileResponse: + summary = store.feedback_logger.summary() + if not summary.feedback_file_path.exists(): + raise HTTPException(status_code=404, detail="No ML feedback data has been collected yet.") + return FileResponse( + summary.feedback_file_path, + media_type="application/x-ndjson; charset=utf-8", + filename=build_feedback_export_filename(), + ) + + @app.delete("/api/ml-feedback", response_model=FeedbackSummaryResponse) + def clear_feedback_history() -> FeedbackSummaryResponse: + summary = store.feedback_logger.clear() + return FeedbackSummaryResponse( + feedback_file_path=str(summary.feedback_file_path), + event_count=summary.event_count, + size_bytes=summary.size_bytes, + export_url=summary.export_url, + ) + + @app.post("/api/analysis-sessions/{session_id}/delete-executions", response_model=DeleteExecutionResponse) + def execute_delete(session_id: str, payload: DeleteExecutionRequest) -> DeleteExecutionResponse: + _get_session_or_404(session_id) + execution = store.execute_delete(session_id, payload.folder_ids) + return DeleteExecutionResponse( + moved_count=execution.moved_count, + failed_count=execution.failed_count, + total_size_mb=execution.total_size_mb, + results=[ + DeleteExecutionItemModel( + folder_id=result.folder_id, + folder_path=str(result.folder_path), + success=result.success, + message=result.message, + size_mb=result.size_mb, + ) + for result in execution.results + ], + ) + + @app.post("/api/analysis-sessions/{session_id}/delete-single", response_model=DeleteExecutionResponse) + def delete_single(session_id: str, payload: SingleDeleteRequest) -> DeleteExecutionResponse: + _get_session_or_404(session_id) + try: + execution = store.execute_single_delete(session_id, payload.cluster_id, payload.folder_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + return DeleteExecutionResponse( + moved_count=execution.moved_count, + failed_count=execution.failed_count, + total_size_mb=execution.total_size_mb, + results=[ + DeleteExecutionItemModel( + folder_id=result.folder_id, + folder_path=str(result.folder_path), + success=result.success, + message=result.message, + size_mb=result.size_mb, + ) + for result in execution.results + ], + ) + + @app.post("/api/system/open-explorer") + def open_explorer(payload: OpenExplorerRequest) -> dict: + path = Path(payload.path).resolve() + if not path.exists(): + raise HTTPException(status_code=400, detail="Path does not exist.") + if not hasattr(os, "startfile"): + raise HTTPException(status_code=501, detail="Explorer integration is only available on Windows.") + os.startfile(str(path)) + return {"status": "opened", "path": str(path)} + + @app.get("/api/analysis-sessions/{session_id}/albums/{folder_id}/cover") + def get_album_cover(session_id: str, folder_id: str): + session = _get_session_or_404(session_id) + album, folder_info = _get_album_context_or_404(session, folder_id) + cover_file = _find_cover_file(album.path) + if cover_file: + return FileResponse(cover_file) + + embedded_art = _read_embedded_album_art(folder_info) + if embedded_art is None: + raise HTTPException(status_code=404, detail="Album cover is not available.") + + content, media_type = embedded_art + return Response(content=content, media_type=media_type) + + @app.get("/api/analysis-sessions/{session_id}/albums/{folder_id}/tracks/{track_index}/stream") + def stream_track(session_id: str, folder_id: str, track_index: int): + session = _get_session_or_404(session_id) + _album, folder_info = _get_album_context_or_404(session, folder_id) + if track_index < 0 or track_index >= len(folder_info.files): + raise HTTPException(status_code=404, detail="Track not found.") + + track_path = folder_info.files[track_index].filepath.resolve() + if not track_path.is_file(): + raise HTTPException(status_code=404, detail="Track file is missing.") + return FileResponse(track_path) + + return app + + +def _get_session_or_404(session_id: str): + session = store.get_session(session_id) + if session is None: + _raise_404(session_id) + return session + + +def _raise_404(session_id: str): + raise HTTPException(status_code=404, detail=f"Unknown session id: {session_id}") + + +def _cluster_model(session, cluster_id: str) -> ClusterSummaryModel: + cluster = session.snapshot.clusters[cluster_id] + albums = [ + FolderSummaryModel( + folder_id=album.folder_id, + path=str(album.path), + name=album.name, + quality_score=album.quality_score, + avg_bitrate=album.avg_bitrate, + file_count=album.file_count, + in_preferred_root=album.in_preferred_root, + has_album_art=album.has_album_art, + lossless_ratio=album.lossless_ratio, + lyrics_ratio=album.lyrics_ratio, + total_size_mb=album.total_size_mb, + is_deleted=album.folder_id in session.deleted_folder_ids, + album_art_preview_url=( + f"/api/analysis-sessions/{session.session_id}/albums/{album.folder_id}/cover" + if album.has_album_art + else None + ), + tracks=[ + TrackInfoModel( + track_index=index, + filename=file_info.filename, + filepath=str(file_info.filepath), + title=file_info.title, + artist=file_info.artist, + album=file_info.album, + duration=file_info.duration, + size_mb=file_info.size_mb, + bitrate=file_info.bitrate, + stream_url=( + f"/api/analysis-sessions/{session.session_id}/albums/{album.folder_id}/tracks/{index}/stream" + ), + ) + for index, file_info in enumerate(session.snapshot.folders[album.path].files) + ], + ) + for album in (session.snapshot.albums[folder_id] for folder_id in cluster.folder_ids) + ] + pairs = [ + PairScoreBreakdownModel( + pair_id=pair.pair_id, + folder1_id=pair.folder1_id, + folder2_id=pair.folder2_id, + algorithmic_score=pair.algorithmic_score, + ml_score=pair.ml_score, + base_score=pair.base_score, + gemini_score=pair.gemini_score, + final_score=pair.final_score, + gemini_verdict=pair.gemini_verdict, + gemini_reason=pair.gemini_reason, + gemini_error=pair.gemini_error, + is_identical_by_hash=pair.is_identical_by_hash, + similarity_scores=pair.similarity_scores, + reason_codes=pair.reason_codes, + ) + for pair in (session.snapshot.pairs[pair_id] for pair_id in cluster.pair_ids) + ] + return ClusterSummaryModel( + cluster_id=cluster.cluster_id, + confidence_bucket=cluster.confidence_bucket, + recommended_keeper_id=cluster.recommended_keeper_id, + selected_keeper_id=session.decisions.get(cluster_id), + selected_delete_folder_ids=sorted(session.delete_selections.get(cluster_id, set())), + human_summary=cluster.human_summary, + resolution_state=cluster.resolution_state, + recommended_keeper_reason=cluster.recommended_keeper_reason, + reason_codes=cluster.reason_codes, + reasons=[ + RecommendationReasonModel(code=reason.code, message=reason.message) + for reason in cluster.reasons + ], + comparison_highlights=[ + ComparisonHighlightModel( + id=highlight.id, + label=highlight.label, + album_id=highlight.album_id, + tone=highlight.tone, + value=highlight.value, + ) + for highlight in cluster.comparison_highlights + ], + technical_summary=cluster.technical_summary, + deletable_folder_ids=cluster.deletable_folder_ids, + albums=albums, + pairs=pairs, + ) + + +def _preview_model(preview) -> DeletePreviewResponse: + return DeletePreviewResponse( + items=[ + DeletePreviewItemModel( + folder_id=item.folder_id, + folder_path=str(item.folder_path), + folder_name=item.folder_name, + keeper_folder_id=item.keeper_folder_id, + keeper_folder_name=item.keeper_folder_name, + keeper_folder_path=str(item.keeper_folder_path), + cluster_id=item.cluster_id, + estimated_size_mb=item.estimated_size_mb, + selection_source=item.selection_source, + ) + for item in preview.items + ], + total_count=preview.total_count, + total_size_mb=preview.total_size_mb, + auto_selected_count=preview.auto_selected_count, + manual_selected_count=preview.manual_selected_count, + ) + + +def _get_album_context_or_404(session, folder_id: str): + album = session.snapshot.albums.get(folder_id) + if album is None: + raise HTTPException(status_code=404, detail=f"Unknown album id: {folder_id}") + + folder_info = session.snapshot.folders.get(album.path) + if folder_info is None: + raise HTTPException(status_code=404, detail="Album content is unavailable.") + return album, folder_info + + +def _find_cover_file(folder_path: Path) -> Path | None: + for filename in sorted(config.ALBUM_ART_FILES): + cover_file = folder_path / filename + if cover_file.is_file(): + return cover_file.resolve() + return None + + +def _read_embedded_album_art(folder_info) -> tuple[bytes, str] | None: + for file_info in folder_info.files[:5]: + art_payload = _read_embedded_art_from_track(file_info.filepath) + if art_payload is not None: + return art_payload + return None + + +def _read_embedded_art_from_track(track_path: Path) -> tuple[bytes, str] | None: + try: + ext = track_path.suffix.lower() + if ext == ".mp3": + audio = MP3(track_path, ID3=ID3) + if audio.tags: + pictures = audio.tags.getall("APIC") + if pictures: + picture = pictures[0] + return picture.data, picture.mime or "image/jpeg" + if ext == ".flac": + audio = FLAC(track_path) + if audio.pictures: + picture = audio.pictures[0] + return picture.data, picture.mime or "image/jpeg" + except (ID3NoHeaderError, MP3HeaderNotFoundError, MutagenError, OSError): + return None + return None + + +app = create_app() diff --git a/album_deduplicator/api/schemas.py b/album_deduplicator/api/schemas.py new file mode 100644 index 0000000..ec151dd --- /dev/null +++ b/album_deduplicator/api/schemas.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Literal, Optional + +from pydantic import BaseModel, Field + + +class AnalysisSessionCreateRequest(BaseModel): + folders: List[str] + preferred_root: Optional[str] = None + preferred_roots: Optional[List[str]] = None + use_preferred_roots: bool = True + force_rescan: bool = False + clear_cache: bool = False + full_hash_scan: bool = False + bitrate_mode: Literal["128", "high"] = "128" + gemini_enabled: bool = False + gemini_api_key: Optional[str] = None + + +class GeminiSettingsResponse(BaseModel): + has_api_key: bool = False + + +class AnalysisSessionCreatedResponse(BaseModel): + session_id: str + status: Literal["queued"] + + +class ProgressState(BaseModel): + step: str = "queued" + stage: str = "queued" + message: str = "ממתין" + human_message: str = "ממתין לתחילת הניתוח." + current: int = 0 + total: int = 1 + percent: float = 0.0 + warnings: List[str] = Field(default_factory=list) + + +class ModeSummary(BaseModel): + ml_default_enabled: bool = True + gemini_enabled: bool = False + review_threshold: float + safe_delete_threshold: float + + +class DegradedFlags(BaseModel): + ml_unavailable: bool = False + gemini_unavailable: bool = False + warnings: List[str] = Field(default_factory=list) + + +class CountsResponse(BaseModel): + folders: int = 0 + compared_pairs: int = 0 + safe_clusters: int = 0 + review_clusters: int = 0 + + +class SessionStatusResponse(BaseModel): + session_id: str + status: Literal["queued", "running", "completed", "failed"] + progress: ProgressState + mode_summary: ModeSummary + degraded_flags: DegradedFlags + counts: CountsResponse + error: Optional[str] = None + + +class RecommendationReasonModel(BaseModel): + code: str + message: str + + +class TrackInfoModel(BaseModel): + track_index: int + filename: str + filepath: str + title: Optional[str] = None + artist: Optional[str] = None + album: Optional[str] = None + duration: Optional[float] = None + size_mb: float + bitrate: Optional[int] = None + stream_url: Optional[str] = None + + +class FolderSummaryModel(BaseModel): + folder_id: str + path: str + name: str + quality_score: Optional[float] + avg_bitrate: float + file_count: int + in_preferred_root: bool + has_album_art: bool + lossless_ratio: float + lyrics_ratio: float + total_size_mb: float + is_deleted: bool = False + album_art_preview_url: Optional[str] = None + tracks: List[TrackInfoModel] = Field(default_factory=list) + + +class PairScoreBreakdownModel(BaseModel): + pair_id: str + folder1_id: str + folder2_id: str + algorithmic_score: float + ml_score: Optional[float] + base_score: float + gemini_score: Optional[float] + final_score: float + gemini_verdict: Optional[str] + gemini_reason: Optional[str] + gemini_error: Optional[str] + is_identical_by_hash: bool + similarity_scores: Dict[str, Any] = Field(default_factory=dict) + reason_codes: List[str] = Field(default_factory=list) + + +class ComparisonHighlightModel(BaseModel): + id: str + label: str + album_id: Optional[str] = None + tone: Literal["positive", "negative", "neutral", "warning"] + value: Optional[str] = None + + +class ClusterSummaryModel(BaseModel): + cluster_id: str + confidence_bucket: Literal["safe", "review"] + recommended_keeper_id: Optional[str] + selected_keeper_id: Optional[str] = None + selected_delete_folder_ids: List[str] = Field(default_factory=list) + human_summary: str + resolution_state: Literal["auto", "user_selected", "skipped", "deleted"] + recommended_keeper_reason: Optional[str] = None + reason_codes: List[str] = Field(default_factory=list) + reasons: List[RecommendationReasonModel] = Field(default_factory=list) + comparison_highlights: List[ComparisonHighlightModel] = Field(default_factory=list) + technical_summary: str = "" + deletable_folder_ids: List[str] = Field(default_factory=list) + albums: List[FolderSummaryModel] = Field(default_factory=list) + pairs: List[PairScoreBreakdownModel] = Field(default_factory=list) + + +class ClusterListResponse(BaseModel): + clusters: List[ClusterSummaryModel] = Field(default_factory=list) + + +class DecisionItem(BaseModel): + cluster_id: str + keeper_id: Optional[str] = None + delete_folder_ids: Optional[List[str]] = None + + +class DecisionsRequest(BaseModel): + decisions: List[DecisionItem] = Field(default_factory=list) + + +class DeletePreviewItemModel(BaseModel): + folder_id: str + folder_path: str + folder_name: str + keeper_folder_id: str + keeper_folder_name: str + keeper_folder_path: str + cluster_id: str + estimated_size_mb: float = 0.0 + selection_source: Literal["auto", "user_selected", "skipped", "deleted"] = "auto" + + +class DeletePreviewResponse(BaseModel): + items: List[DeletePreviewItemModel] = Field(default_factory=list) + total_count: int = 0 + total_size_mb: float = 0.0 + auto_selected_count: int = 0 + manual_selected_count: int = 0 + + +class DeleteExecutionRequest(BaseModel): + folder_ids: List[str] = Field(default_factory=list) + + +class DeleteExecutionItemModel(BaseModel): + folder_id: str + folder_path: str + success: bool + message: str + size_mb: float = 0.0 + + +class DeleteExecutionResponse(BaseModel): + moved_count: int + failed_count: int + total_size_mb: float = 0.0 + results: List[DeleteExecutionItemModel] = Field(default_factory=list) + + +class FeedbackSummaryResponse(BaseModel): + feedback_file_path: str + event_count: int = 0 + size_bytes: int = 0 + export_url: str + + +class SingleDeleteRequest(BaseModel): + cluster_id: str + folder_id: str + + +class OpenExplorerRequest(BaseModel): + path: str diff --git a/album_deduplicator/api/session_store.py b/album_deduplicator/api/session_store.py new file mode 100644 index 0000000..8a28bae --- /dev/null +++ b/album_deduplicator/api/session_store.py @@ -0,0 +1,412 @@ +from __future__ import annotations + +import logging +from collections import deque +import queue +import threading +import uuid +from dataclasses import dataclass, field +from typing import Dict, Optional, Set + +from music_dup_lib.services import ( + AnalysisOptions, + AnalysisOrchestrator, + AnalysisProgressEvent, + AnalysisSnapshot, + DeletionService, + DeleteExecution, + DeletePreview, + UserDecisionStore, + UserFeedbackLogger, + GeminiSettingsStore, +) + +logger = logging.getLogger(__name__) +MAX_SESSION_EVENTS = 256 + + +class SessionEventBuffer: + def __init__(self, maxlen: int = MAX_SESSION_EVENTS): + self._events: deque[dict] = deque() + self._maxlen = maxlen + self._lock = threading.Lock() + + def put(self, event: dict) -> None: + with self._lock: + if event.get("event") == "progress": + self._drop_latest_progress_locked() + elif len(self._events) >= self._maxlen: + self._drop_oldest_progress_locked() + + if len(self._events) >= self._maxlen: + self._events.popleft() + + self._events.append(event) + + def get_nowait(self) -> dict: + with self._lock: + if not self._events: + raise queue.Empty + return self._events.popleft() + + def empty(self) -> bool: + with self._lock: + return not self._events + + def _drop_latest_progress_locked(self) -> None: + for index in range(len(self._events) - 1, -1, -1): + if self._events[index].get("event") == "progress": + del self._events[index] + return + + def _drop_oldest_progress_locked(self) -> None: + for index, buffered_event in enumerate(self._events): + if buffered_event.get("event") == "progress": + del self._events[index] + return + + +@dataclass +class SessionState: + session_id: str + options: AnalysisOptions + status: str = "queued" + progress: dict = field( + default_factory=lambda: { + "step": "queued", + "stage": "queued", + "message": "ממתין", + "human_message": "ממתין לתחילת הניתוח.", + "current": 0, + "total": 1, + "percent": 0.0, + "warnings": [], + } + ) + snapshot: AnalysisSnapshot = field(default_factory=AnalysisSnapshot) + decisions: Dict[str, Optional[str]] = field(default_factory=dict) + preview: DeletePreview = field(default_factory=DeletePreview) + error: Optional[str] = None + events: SessionEventBuffer = field(default_factory=SessionEventBuffer) + lock: threading.Lock = field(default_factory=threading.Lock) + resolution_states: Dict[str, str] = field(default_factory=dict) + deleted_folder_ids: Set[str] = field(default_factory=set) + delete_selections: Dict[str, Set[str]] = field(default_factory=dict) + + +class SessionStore: + def __init__(self, decision_store: Optional[UserDecisionStore] = None): + self._sessions: Dict[str, SessionState] = {} + self._orchestrator = AnalysisOrchestrator() + self._deletion_service = DeletionService() + self._decision_store = decision_store or UserDecisionStore() + self.gemini_settings_store = GeminiSettingsStore() + self.feedback_logger = UserFeedbackLogger() + self._lock = threading.Lock() + + def create_session(self, options: AnalysisOptions) -> SessionState: + session = SessionState(session_id=uuid.uuid4().hex, options=options) + with self._lock: + self._sessions[session.session_id] = session + return session + + def get_session(self, session_id: str) -> Optional[SessionState]: + return self._sessions.get(session_id) + + def start_analysis(self, session_id: str) -> None: + session = self._require_session(session_id) + + def target() -> None: + self.run_analysis_sync(session_id) + + thread = threading.Thread(target=target, daemon=True) + thread.start() + + def run_analysis_sync(self, session_id: str) -> None: + session = self._require_session(session_id) + with session.lock: + session.status = "running" + self._push_event(session, "status", {"status": "running"}) + try: + snapshot = self._orchestrator.run( + session.options, + progress_handler=lambda event: self._handle_progress(session, event), + ) + decisions = { + cluster_id: cluster.recommended_keeper_id if cluster.confidence_bucket == "safe" else None + for cluster_id, cluster in snapshot.clusters.items() + } + resolution_states = { + cluster_id: "auto" if cluster.confidence_bucket == "safe" and cluster.recommended_keeper_id else "skipped" + for cluster_id, cluster in snapshot.clusters.items() + } + delete_selections = { + cluster_id: self._default_delete_selection( + cluster, + decisions.get(cluster_id), + set(), + ) + for cluster_id, cluster in snapshot.clusters.items() + } + for cluster_id, stored_decision in self._decision_store.decisions_for_snapshot(snapshot).items(): + cluster = snapshot.clusters[cluster_id] + decisions[cluster_id] = stored_decision.keeper_id + resolution_states[cluster_id] = stored_decision.resolution_state + delete_selections[cluster_id] = self._normalize_delete_selection( + cluster=cluster, + keeper_id=stored_decision.keeper_id, + requested_folder_ids=stored_decision.delete_folder_ids, + deleted_folder_ids=set(), + ) + self._apply_resolution_states(snapshot, resolution_states, set()) + preview = self._deletion_service.build_preview( + clusters=snapshot.clusters, + albums=snapshot.albums, + decisions=decisions, + resolution_states=resolution_states, + delete_selections=delete_selections, + ) + with session.lock: + session.snapshot = snapshot + session.decisions = decisions + session.resolution_states = resolution_states + session.delete_selections = delete_selections + session.preview = preview + session.status = "completed" + session.progress = { + "step": "completed", + "stage": "complete", + "message": "הניתוח הושלם", + "human_message": "הניתוח הסתיים. אפשר להתחיל לעבור על הקבוצות הבטוחות והקבוצות שדורשות סקירה.", + "current": 1, + "total": 1, + "percent": 100.0, + "warnings": snapshot.warnings.warnings, + } + self._push_event(session, "completed", {"status": "completed"}) + except Exception as exc: + logger.exception("Analysis session failed: %s", session_id) + with session.lock: + session.status = "failed" + session.error = str(exc) + self._push_event(session, "failed", {"status": "failed", "error": str(exc)}) + + def apply_decisions( + self, + session_id: str, + decisions: Dict[str, Optional[str]], + delete_selections: Optional[Dict[str, Set[str]]] = None, + ) -> DeletePreview: + session = self._require_session(session_id) + with session.lock: + for cluster_id, keeper_id in decisions.items(): + previous_keeper_id = session.decisions.get(cluster_id) + session.decisions[cluster_id] = keeper_id + session.resolution_states[cluster_id] = "user_selected" if keeper_id else "skipped" + cluster = session.snapshot.clusters.get(cluster_id) + if cluster is None: + session.delete_selections[cluster_id] = set() + continue + if keeper_id is None: + session.delete_selections[cluster_id] = set() + continue + requested_selection = None + if delete_selections and cluster_id in delete_selections: + requested_selection = delete_selections[cluster_id] + elif previous_keeper_id == keeper_id and cluster_id in session.delete_selections: + requested_selection = session.delete_selections[cluster_id] + session.delete_selections[cluster_id] = self._normalize_delete_selection( + cluster=cluster, + keeper_id=keeper_id, + requested_folder_ids=requested_selection, + deleted_folder_ids=session.deleted_folder_ids, + ) + self._apply_resolution_states(session.snapshot, session.resolution_states, session.deleted_folder_ids) + session.preview = self._deletion_service.build_preview( + clusters=session.snapshot.clusters, + albums=session.snapshot.albums, + decisions=session.decisions, + resolution_states=session.resolution_states, + delete_selections=session.delete_selections, + excluded_folder_ids=session.deleted_folder_ids, + ) + for cluster_id, keeper_id in decisions.items(): + if keeper_id: + self._decision_store.save_decision( + snapshot=session.snapshot, + cluster_id=cluster_id, + keeper_id=keeper_id, + delete_folder_ids=session.delete_selections.get(cluster_id, set()), + resolution_state=session.resolution_states.get(cluster_id, "user_selected"), + ) + else: + self._decision_store.clear_decision(cluster_id) + return session.preview + + def get_preview(self, session_id: str) -> DeletePreview: + session = self._require_session(session_id) + return session.preview + + def execute_delete(self, session_id: str, folder_ids: list[str]) -> DeleteExecution: + session = self._require_session(session_id) + preview_items = list(session.preview.items) + execution = self._deletion_service.execute(session.preview, folder_ids) + removed_ids = {result.folder_id for result in execution.results if result.success} + with session.lock: + self.feedback_logger.log_delete_execution( + session_id=session.session_id, + snapshot=session.snapshot, + execution=execution, + preview_items=preview_items, + source="bulk_delete", + ) + session.deleted_folder_ids.update(removed_ids) + for selection in session.delete_selections.values(): + selection.difference_update(removed_ids) + session.preview = self._deletion_service.build_preview( + clusters=session.snapshot.clusters, + albums=session.snapshot.albums, + decisions=session.decisions, + resolution_states=session.resolution_states, + delete_selections=session.delete_selections, + excluded_folder_ids=session.deleted_folder_ids, + ) + self._apply_resolution_states(session.snapshot, session.resolution_states, session.deleted_folder_ids) + self._push_event( + session, + "delete_execution", + { + "moved_count": execution.moved_count, + "failed_count": execution.failed_count, + "total_size_mb": execution.total_size_mb, + }, + ) + return execution + + def execute_single_delete(self, session_id: str, cluster_id: str, folder_id: str) -> DeleteExecution: + session = self._require_session(session_id) + with session.lock: + cluster = session.snapshot.clusters.get(cluster_id) + if cluster is None: + raise KeyError(f"Unknown cluster id: {cluster_id}") + keeper_id = ( + session.decisions[cluster_id] + if cluster_id in session.decisions + else cluster.recommended_keeper_id + ) + if keeper_id is None: + raise ValueError("לא ניתן למחוק בודד בלי keeper פעיל לקבוצה.") + if folder_id == keeper_id: + raise ValueError("לא ניתן למחוק את העותק שנבחר לשמירה.") + if folder_id not in cluster.folder_ids: + raise ValueError("התיקייה שנבחרה אינה חלק מהקבוצה.") + preview_item = self._deletion_service.build_preview_item( + cluster=cluster, + albums=session.snapshot.albums, + folder_id=folder_id, + keeper_id=keeper_id, + selection_source="user_selected", + ) + + preview_items = [preview_item] + execution = self._deletion_service.execute(DeletePreview(items=preview_items), [folder_id]) + removed_ids = {result.folder_id for result in execution.results if result.success} + with session.lock: + self.feedback_logger.log_delete_execution( + session_id=session.session_id, + snapshot=session.snapshot, + execution=execution, + preview_items=preview_items, + source="single_delete", + ) + session.deleted_folder_ids.update(removed_ids) + for selection in session.delete_selections.values(): + selection.difference_update(removed_ids) + session.preview = self._deletion_service.build_preview( + clusters=session.snapshot.clusters, + albums=session.snapshot.albums, + decisions=session.decisions, + resolution_states=session.resolution_states, + delete_selections=session.delete_selections, + excluded_folder_ids=session.deleted_folder_ids, + ) + self._apply_resolution_states(session.snapshot, session.resolution_states, session.deleted_folder_ids) + self._push_event( + session, + "delete_execution", + { + "moved_count": execution.moved_count, + "failed_count": execution.failed_count, + "total_size_mb": execution.total_size_mb, + }, + ) + return execution + + def _handle_progress(self, session: SessionState, event: AnalysisProgressEvent) -> None: + total = max(event.total, 1) + percent = round((event.current / total) * 100, 2) + progress = { + "step": event.step, + "stage": event.stage, + "message": event.message, + "human_message": event.human_message, + "current": event.current, + "total": total, + "percent": percent, + "warnings": list(session.snapshot.warnings.warnings), + } + with session.lock: + session.progress = progress + self._push_event(session, "progress", progress) + + def _push_event(self, session: SessionState, event_name: str, data: dict) -> None: + session.events.put({"event": event_name, "data": data}) + + def _require_session(self, session_id: str) -> SessionState: + session = self.get_session(session_id) + if session is None: + raise KeyError(f"Unknown session id: {session_id}") + return session + + def _apply_resolution_states( + self, + snapshot: AnalysisSnapshot, + resolution_states: Dict[str, str], + deleted_folder_ids: Set[str], + ) -> None: + for cluster_id, cluster in snapshot.clusters.items(): + if cluster.deletable_folder_ids and all( + folder_id in deleted_folder_ids for folder_id in cluster.deletable_folder_ids + ): + cluster.resolution_state = "deleted" + resolution_states[cluster_id] = "deleted" + else: + cluster.resolution_state = resolution_states.get(cluster_id, cluster.resolution_state) + + def _default_delete_selection( + self, + cluster, + keeper_id: Optional[str], + deleted_folder_ids: Set[str], + ) -> Set[str]: + if keeper_id is None: + return set() + return { + folder_id + for folder_id in cluster.folder_ids + if folder_id != keeper_id and folder_id not in deleted_folder_ids + } + + def _normalize_delete_selection( + self, + cluster, + keeper_id: Optional[str], + requested_folder_ids: Optional[Set[str]], + deleted_folder_ids: Set[str], + ) -> Set[str]: + if keeper_id is None or keeper_id not in cluster.folder_ids: + return set() + allowed_folder_ids = self._default_delete_selection(cluster, keeper_id, deleted_folder_ids) + if requested_folder_ids is None: + return allowed_folder_ids + return {folder_id for folder_id in requested_folder_ids if folder_id in allowed_folder_ids} diff --git a/album_deduplicator/api_server.py b/album_deduplicator/api_server.py new file mode 100644 index 0000000..7dd74ef --- /dev/null +++ b/album_deduplicator/api_server.py @@ -0,0 +1,2 @@ +from api.app import app + diff --git a/album_deduplicator/docs/ARCHITECTURE.md b/album_deduplicator/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8402da5 --- /dev/null +++ b/album_deduplicator/docs/ARCHITECTURE.md @@ -0,0 +1,642 @@ +# Album Deduplicator Architecture + +מסמך זה מתאר את מבנה המערכת העדכני של `album_deduplicator` לאחר המעבר ל-`Electron + React + FastAPI`, ואת גבולות האחריות בין שכבות הקוד. + +## מטרות המערכת + +המערכת נועדה: + +- לסרוק תיקיות מוזיקה ולזהות אלבומים פוטנציאליים. +- להשוות בין אלבומים באותה ספרייה לוגית. +- לשלב scoring מתמטי, `ML` מקומי, ו-`Gemini` אופציונלי לזוגות גבוליים. +- להמליץ על `keeper` אחד לכל cluster. +- להציע מחיקה אוטומטית רק כאשר רמת הוודאות גבוהה מאוד. +- לבצע מחיקה אל `Recycle Bin` בלבד. + +## תמונת על + +המערכת בנויה מ-6 שכבות עיקריות: + +1. `Core`: + מנועי הסריקה, חילוץ המטא-דאטה, ההשוואה והאיכות. +2. `Services`: + שכבת orchestration ו-business rules מעל ה-core. +3. `API`: + `FastAPI` עם sessions, DTOs ו-`SSE`. +4. `Desktop Shell`: + `Electron` שמנהל את חלון האפליקציה, מרים backend מקומי, וחושף יכולות מערכת דרך `preload`. +5. `Frontend`: + ממשק `React` שרץ ב-renderer של Electron ומתקשר מול ה-API. +6. `Entrypoints`: + `Electron Desktop`, `CLI`, ושרת API. + +## מבנה תיקיות + +### `music_dup_lib/core` + +אחראי על פעולות עיבוד בסיסיות: + +- `file_processor.py` + - עיבוד קובץ מוזיקה בודד. + - חילוץ מטא-דאטה. + - פתיחה מפורטת אחת בלבד לכל קובץ כאשר נדרש `albumartist` או זיהוי `lyrics`, במקום כמה פתיחות חוזרות. + - הימנעות מקריאות `stat` כפולות וממעברים כפולים על אותם tags, כדי לצמצם overhead פר-קובץ בלי לשנות תוצאות. + - חישוב `partial hash` כברירת מחדל. + - חישוב `full hash` אופציונלי כאשר המשתמש מפעיל אותו בהגדרות המתקדמות. + - איתור hash של עטיפת אלבום תוך reuse של רשימת קבצי התיקייה שכבר נאספה בסריקה. +- `folder_scanner.py` + - סריקה רקורסיבית של roots. + - זיהוי `leaf folders` תקפים בלבד, כדי לא לעבד intermediate directories לשווא. + - עיבוד זורם עם `bounded futures`, כדי למנוע backlog גדול של משימות וזיכרון מיותר. + - reuse של cache קיים וכתיבה קומפקטית שלו בסוף הסריקה. + - מדלג על rewrite של `music_data.json` כאשר כל תוצאות הסריקה כבר הגיעו מה-cache ולא נוצרו שינויים. +- `comparison_engine.py` + - השוואת זוג תיקיות. + - יוצר זוגות להשוואה רק בתוך קבוצות עם אותו מספר קבצי מוזיקה, כי זוגות אחרים ייפסלו בכל מקרה. + - ממחזר תוצאות זוגות שכבר קיימות ב-`comparison_results_cache.pkl` עוד לפני `compare_two_folders`, כדי לחסוך compare עמוק בהרצות `warm cache`. + - ממחזר prepared folder data כמו סדר קבצים ממוין ומיפויי `other files`, כדי לא לחשב את אותן הכנות שוב ושוב לכל זוג. + - מנרמל metadata נוסף של כל קובץ פעם אחת בזמן הכנת התיקייה, במקום לבצע `set intersections` ו-normalization מחדש בכל זוג. + - חישוב `weighted_score` האלגוריתמי. + - דילוג על זוגות עם מספר שירי מוזיקה שונה. +- `quality_analyzer.py` + - חישוב `quality_score` לכל אלבום. +- `data_store.py` + - cache של תיקיות ושל תוצאות השוואה. + +### footprint בזיכרון + +- מודלי `FileInfo`, `FolderInfo`, ו-`FolderComparisonResult` מוגדרים כעת עם `slots`, כדי להקטין overhead פר-אובייקט במהלך סריקות גדולות. + +### `music_dup_lib/external` + +אינטגרציות חיצוניות: + +- `ml_similarity_model.py` + - טוען מודל `LightGBM`. + - מחפש קודם ב-`album_deduplicator/data`. + - fallback אוטומטי ל-`similarity_model/models`. + - ממחזר folder-level features במקום לחשב אותם מחדש לכל זוג. + - מריץ inference ב-`numpy batches` ומגביל threads של `LightGBM` בזמן prediction. +- `gemini_analyzer.py` + - ניתוח זוגות גבוליים מול `Gemini`. + - מקבל מפתח API פר-session מתוך מסך ההגדרה או נופל חזרה אל `GEMINI_API_KEY`. + - מקבל `base_score` כקלט תומך. + - מחזיר verdict, ציון, והסבר. + +### `music_dup_lib/services` + +זו השכבה העסקית החדשה: + +- `analysis_orchestrator.py` + - entrypoint לניתוח מלא. + - מריץ scan, compare, score, cluster. + - טוען cache של comparisons לפני שלב ההשוואה ומעביר אותו ל-`ComparisonEngine`, כך שזוגות שכבר חושבו לא יעברו compare עמוק שוב. + - ממחזר את אותו comparison cache גם בזמן save, בלי `load` נוסף של אותו קובץ. + - מדווח progress לשכבות מעליו. +- `scoring_service.py` + - מחשב `algorithmic_score`, `ml_score`, `base_score`, `gemini_score`, `final_score`. + - מפעיל `Gemini` רק בטווח הגבולי. + - מסמן degraded mode כאשר ML או Gemini אינם זמינים. + - מבצע batching של חישובי ML כדי לצמצם spike של CPU ו-RAM בפרויקטים גדולים. + - מדלג על ML לזוגות שהם כבר `identical_by_hash`. + - שומר ב-memory רק pairs שיכולים להשפיע על clusters, ולא את כל הזוגות החלשים. + - משחרר `comparison_results` שכבר עובדו ושומר cache רזה ל-ML/Gemini בלבד. +- `recommendation_service.py` + - יוצר `AlbumSummary`. + - בונה graph של קשרי דמיון. + - מחלץ `clusters`. + - בוחר `keeper` לפי: + - `preferred_root` + - אחר כך `quality_score` + - מסווג cluster ל-`safe` או `review`. +- `deletion_service.py` + - בונה `delete preview`. + - מבצע `send2trash`. +- `user_feedback_logger.py` + - כותב אירועי החלטות משתמש לקובץ `JSONL` בנתיב user-data גלובלי שמתאים גם להתקנות ארוזות. + - מתעד מחיקות מוצלחות כ-`same_album_confirmed` עם `evidence_strength=strong`. + - אינו מתעד סימוני keeper/מחיקה זמניים או החלטת `שמור את כל העותקים`, כדי להימנע מ-labels שהמשתמש עוד יכול לשנות. + - שומר יחד עם כל אירוע את ה-cluster, ה-keeper, ה-target, ציוני הזוגות, וספי/משקלי ה-scoring שהיו בתוקף. +- `user_decision_store.py` + - שומר החלטות משתמש ידניות לקובץ JSON מקומי תחת user-data. + - נשען על `cluster_id` ו-`folder_id` יציבים, ומחיל החלטה מחדש רק כאשר אותה קבוצת תיקיות מופיעה שוב בסריקה חדשה. + - מיועד לשחזור עבודה, ולא משמש כ-dataset אימון. +- `gemini_settings_store.py` + - שומר מפתח `Gemini` מקומי תחת user-data כדי שלא יהיה צורך להזין אותו בכל הפעלה. + - ה-API מחזיר ל-renderer רק האם קיים מפתח שמור, ולא את ערך המפתח עצמו. +- `dto.py` + - אובייקטים יציבים לשכבות העליונות: + - `AnalysisSnapshot` + - `AlbumSummary` + - `PairAnalysis` + - `AlbumCluster` + - `DeletePreview` + - `DeleteExecution` + +### `api` + +שכבת השרת: + +- `api/app.py` + - אפליקציית `FastAPI`. + - endpoints ציבוריים. + - `SSE` לאירועי progress. + - המרת DTOs פנימיים ל-response models. +- `schemas.py` + - חוזי API מבוססי `Pydantic`. +- `session_store.py` + - ניהול `analysis sessions` בזיכרון. + - שמירת progress, snapshot, decisions, סימוני מחיקה פרטניים ו-preview. + - החלת החלטות ידניות שנשמרו מקומית לאחר סריקה חדשה של אותו cluster. + - buffer חסום לאירועי `SSE`, עם coalescing של `progress`, כדי למנוע growth לא מוגבל בזיכרון כאשר אין consumer פעיל. + - background execution לכל session. + +### `frontend/electron` + +מעטפת ה-desktop: + +- `main.cjs` + - יוצר `BrowserWindow` + - טוען את אייקון היישום מתוך נכסי `electron/assets/icons`, כך שגם חלון הפיתוח וגם החבילה הארוזה משתמשים באותו icon + - מרים `FastAPI` כתהליך מקומי + - ממתין ל-`GET /api/health` + - מנהל lifecycle של backend בעת פתיחה/סגירה +- `preload.cjs` + - חושף bridge מאובטח ל-renderer + - בחירת תיקיות native, כולל multi-select ל-roots של הסריקה + - פתיחת נתיבים ב-Explorer + - הזרקת runtime metadata כמו `backendBaseUrl` + +### `frontend` + +הממשק החדש: + +- `src/App.jsx` + - flow ראשי. + - עוטף את כל ה־renderer ב־`HappyProvider` + `ConfigProvider` של `Ant Design`. + - מגדיר `RTL`, theme tokens, ו-notifications דרך `AntD App`. + - מציג `workflow rail` גלובלי ודביק בחלק העליון של ה-shell, עם מעבר בין `setup`, `scan`, `summary`, `review`, ו-`finalize` לפי מצב ה-session. + - מזהה אם היישום רץ בתוך `Electron` + - יצירת session. + - האזנה ל-`SSE`. + - ניהול roots בשדות נפרדים עם add/remove ברור. + - fallback ידני להזנת roots גם ב-Desktop כאשר chooser native של Windows נכשל עבור תיקיות מסוימות. + - חזרה ל-`setup` דרך ה-rail משאירה את ה-session האחרון זמין ל-`summary` / `review` / `finalize` עד שמתחילים סריקה חדשה בפועל. + - שומר את מזהה ה-session האחרון ב-`localStorage`, כדי ש-reload של ה-renderer יוכל להתחבר שוב ל-session פעיל כל עוד ה-backend המקומי עדיין רץ. + - הצגת tabs: + - `בטוח למחיקה` + - `דורש סקירה` + - `כל התוצאות` + - מסך `setup` נשאר קומפקטי עם שורת root אחת פתוחה כברירת מחדל, בחירה native מרובת תיקיות ב-Desktop, בחירת `preferred_root` מתוך ה-roots שכבר נוספו, ו-section מתקדם קצר עבור `force_rescan`, `Full Hash Scan` ו-`Gemini`. + - תחת הגדרת `Gemini` מופיע שדה סיסמה למפתח API. אם קיים מפתח שמור, המשתמש יכול להשאיר את השדה ריק וה-backend ישתמש במפתח המקומי. + - שינוי keeper ידני. + - בחירת keeper מהירה לפי מספר העותק בכרטיס או במקשי הספרות בזמן review, עם מקש רווח לביטול הכרעה ושמירת כל העותקים. + - סימון פרטני של עותקים למחיקה. + - פתיחה מיידית של נתיבים ב-Explorer. + - מחיקה בודדת מיידית מתוך ה-cluster. + - delete confirmation לפני כל מחיקה בפועל. + - playback ישיר של שירים מתוך טבלת ההשוואה דרך stream URLs של ה-API. + - preview של עטיפות אלבום בכל כרטיס עותק, כולל fallback ל-embedded art כאשר אין קובץ עטיפה חיצוני. +- `src/theme/antdTheme.js` + - seed tokens ו-component tokens של `Ant Design 6`. + - קובע palette, typography, radius, shadows ו-motion עבור שפת `Cartoon Desktop` מעודנת יותר, עם density רגועה ו-border hierarchy מתונה. +- `src/styles.css` + - entrypoint של הסטיילים בפרונטאנד. + - מייבא partials קטנים תחת `src/styles/` עבור shell כללי, מסכי setup/scan/summary, סביבת review, ומסך finalize. + - מגדיר גם `scroll containers` ברורים ברמת ה-shell, review workspace, ו-finalize כדי למנוע clipping כאשר תצוגות ארוכות או רחבות. + - מסכי `scan` ו-`summary` משתמשים ב-grid ייעודי ל-KPI-ים במקום overrides על `Row/Col`, כדי לשמור על layout יציב גם בחלונות צרים או צפופים. + - מסך `scan` כולל גם strip של שלבי ניתוח, אנימציית orbit/equalizer קלה, ו-copy דינמי לפי `stage` פעיל. + - סביבת `review` משתמשת כעת ב-overview דו-עמודי: hero החלטה ראשי + side rail למצב הקבוצה ולשקיפות scoring, כדי לצמצם עומס ויזואלי ולהפריד בין summary, החלטות ותוכן השוואה. + - באזור ה-`review` יש גם audio preview צף וקבוע בתחתית סביבת העבודה ו-preview ויזואלי של עטיפות אלבום, כדי לאפשר אימות ידני מהיר בלי לצאת מהיישום. +- `src/desktop.js` + - abstraction ליכולות desktop ול-runtime metadata. +- `src/App.test.jsx` + - smoke test לממשק הראשי. + +### Entrypoints + +- `main.py` + - CLI חדש. + - משתמש ב-`AnalysisOrchestrator`. +- `api_server.py` + - entrypoint פשוט לשרת API. +- `frontend/electron/main.cjs` + - entrypoint הראשי של אפליקציית ה-desktop. + +## זרימת נתונים מלאה + +### 0. Desktop bootstrap + +במצב desktop: + +1. `Electron main` מוצא port פנוי. +2. מריץ backend מקומי (`FastAPI`) על `127.0.0.1`. +3. בודק readiness דרך `GET /api/health`. +4. טוען את חלון האפליקציה. +5. `preload` מזריק ל-renderer את `backendBaseUrl` ואת יכולות המערכת. + +### 1. יצירת session + +ה-frontend שולח: + +- `POST /api/analysis-sessions` + +השרת: + +- מאמת paths. +- יוצר `SessionState`. +- מתחיל background thread. + +### 2. orchestration + +`AnalysisOrchestrator` מבצע: + +1. יצירת `FileProcessor` +2. יצירת `FolderScanner` +3. סריקת תיקיות root +4. יצירת `ComparisonEngine` +5. חישוב כל הזוגות התקפים +6. העברת התוצאות ל-`ScoringService` +7. בניית clusters עם `RecommendationService` +8. יצירת `AnalysisSnapshot` + +### 3. scoring + +לכל pair: + +- `algorithmic_score` מגיע מ-`comparison_engine`. +- `ml_score` מחושב דרך `MLSimilarityModel`, אם קיים מודל. +- `base_score`: + +```text +base_score = 0.35 * algorithmic_score + 0.65 * ml_score +``` + +אם אין ML: + +```text +base_score = algorithmic_score +``` + +אם `Gemini` פעיל והזוג נמצא בטווח הגבולי: + +```text +final_score = 0.85 * base_score + 0.15 * gemini_score +``` + +אם לא: + +```text +final_score = base_score +``` + +### 4. clustering + +`RecommendationService`: + +- בונה graph בין תיקיות שיש ביניהן: + - `identical_by_hash`, או + - `final_score > REVIEW_MIN_SIMILARITY` +- מוצא connected components. +- כל component עם יותר מאלבום אחד הופך ל-`cluster`. + +### 5. keeper selection + +בחירת `keeper` נעשית לפי: + +1. המיקום של האלבום תחת `preferred_roots`, לפי סדר ההעדפות שנשלח מה-UI, כאשר `use_preferred_roots` פעיל. +2. אם שני אלבומים באותו root מועדף או ללא root מועדף, לפי `quality_score` +3. אם עדיין יש תיקו מהותי, אין `keeper` ברור + +כאשר אין `keeper` ברור: + +- ה-cluster יסווג ל-`review` +- לא תוצע מחיקה אוטומטית + +### 6. delete preview + +לאחר סיום הניתוח: + +- clusters ב-`safe` מקבלים החלטה ראשונית אוטומטית: + - `decision = recommended_keeper_id` +- clusters ב-`safe` מסמנים כברירת מחדל את כל שאר חברי ה-cluster למחיקה. +- clusters ב-`review` מתחילים ב-`skip` +- המשתמש יכול לבטל או להוסיף סימוני מחיקה פרטניים לכל cluster +- `DeletionService` יוצר preview מרוכז של מה יימחק ומה יישמר + +### 7. ביצוע מחיקה + +ה-frontend שולח: + +- `POST /api/analysis-sessions/{session_id}/delete-executions` + +והשרת: + +- מאשר רק `folder_ids` שמופיעים ב-preview +- מבצע `send2trash` +- מחזיר `moved_count`, `failed_count`, `results` + +## מדיניות scoring ומחיקה + +### קבועים מרכזיים + +מוגדרים ב-`music_dup_lib/config.py`: + +- `BASE_SCORE_ALGORITHMIC_WEIGHT = 0.35` +- `BASE_SCORE_ML_WEIGHT = 0.65` +- `FINAL_SCORE_BASE_WEIGHT = 0.85` +- `FINAL_SCORE_GEMINI_WEIGHT = 0.15` +- `REVIEW_MIN_SIMILARITY = 60.0` +- `SAFE_DELETE_MIN_SIMILARITY = 90.0` + +### מתי pair נכנס ל-review + +כאשר: + +- `60 < final_score <= 90`, או +- אין `keeper` ברור, או +- יש חוסר ודאות עסקי בתוך cluster + +### מתי cluster נכנס ל-safe + +רק כאשר כל התנאים מתקיימים: + +- כל הזוגות בתוך הקבוצה אומתו ישירות +- כל הקשרים שנבדקו בטוחים: + - `identical_by_hash`, או + - `final_score > 90` +- יש `keeper` יחיד וברור +- אותו keeper "מכסה" את שאר חברי ה-cluster + +### מתי מחיקה אוטומטית לא מוצעת + +- מספר שירי המוזיקה שונה בין תיקיות +- אין `keeper` ברור +- pair גבולי בלבד +- `Gemini` נתן אינדיקציה אך הסף הבטוח לא הושג + +## חוזי API + +### `POST /api/analysis-sessions` + +יוצר session חדש. + +קלט: + +- `folders[]` +- `preferred_roots` לפי סדר עדיפות, עם `preferred_root` legacy כערך הראשון +- `use_preferred_roots`, פעיל כברירת מחדל; כאשר כבוי סדר ה-roots נשמר אך לא משפיע על בחירת keeper +- `force_rescan` +- `clear_cache` +- `full_hash_scan` +- `bitrate_mode` +- `gemini_enabled` + +פלט: + +- `session_id` +- `status = queued` + +### `GET /api/analysis-sessions/{session_id}` + +מחזיר: + +- `status` +- `progress` +- `mode_summary` +- `degraded_flags` +- `counts` +- `error` + +### `GET /api/analysis-sessions/{session_id}/events` + +`SSE` עם אירועים: + +- `status` +- `progress` +- `completed` +- `failed` +- `delete_execution` +- `end` + +### `GET /api/analysis-sessions/{session_id}/clusters?bucket=safe|review|all` + +מחזיר clusters עם: + +- מידע על albums +- breakdown של pairs +- reason codes +- recommended keeper +- selected keeper בפועל, כאשר המשתמש כבר בחר תיקייה לשמירה או החלטה נשמרה מסריקה קודמת +- selected delete folder ids + +### `POST /api/analysis-sessions/{session_id}/decisions` + +שומר החלטות user: + +- `cluster_id -> keeper_id | null` +- `cluster_id -> delete_folder_ids[]` + +ומחזיר preview חדש. + +### `POST /api/analysis-sessions/{session_id}/delete-single` + +מבצע מחיקה מיידית של תיקייה בודדת מתוך cluster, כל עוד נבחר keeper פעיל. + +### `POST /api/system/open-explorer` + +פותח path ישירות ב-Windows Explorer לטובת אימות ידני מהיר. + +### `GET /api/analysis-sessions/{session_id}/delete-preview` + +מחזיר: + +- מה יימחק +- מה יישמר +- לאיזה cluster כל item שייך + +### `GET /api/analysis-sessions/{session_id}/albums/{folder_id}/cover` + +מחזיר preview של עטיפת אלבום עבור העותק המבוקש: + +- קודם מנסה קובץ עטיפה ידוע מתוך התיקייה +- אם אין קובץ כזה אבל יש embedded art באחד השירים, מחזיר את התמונה המוטמעת +- משמש את ה-frontend ל-preview ולהגדלה מתוך כרטיסי האלבום + +### `GET /api/analysis-sessions/{session_id}/albums/{folder_id}/tracks/{track_index}/stream` + +מחזיר stream של קובץ השיר לפי מיקומו בתוך העותק: + +- ה-frontend משתמש בנתיב הזה לניגון ישיר מתוך טבלת ההשוואה +- הגישה נשארת session-scoped ולא חושפת filesystem paths כ-endpoint ציבורי גנרי + +### `POST /api/analysis-sessions/{session_id}/delete-executions` + +מבצע מחיקה בפועל ומחזיר תוצאה מפורטת. + +### `GET /api/ml-feedback/summary` + +מחזיר את מצב קובץ ה-feedback המקומי: + +- path מלא לקובץ ה-`JSONL` +- מספר אירועים שנשמרו +- גודל הקובץ +- URL ליצוא + +### `GET /api/ml-feedback/export` + +מחזיר את קובץ ה-`JSONL` להורדה בשם תמציתי שמכיל זמן, משתמש, מכונה ומזהה קצר, כדי לאפשר שיתוף ידני של נתוני אימון ולהבדיל בין סריקות שונות. + +### `DELETE /api/ml-feedback` + +מוחק את קובץ ה-feedback המקומי ומחזיר summary ריק. הפעולה זמינה גם מהממשק דרך `נקה היסטוריה`. + +## Cache ו-persistence + +### קבצי cache + +- `data/music_data.json` + - snapshot של תיקיות ו-metadata +- `data/comparison_results_cache.pkl` + - תוצאות comparison וציוני ML/Gemini + +### session state + +`SessionStore` שומר session בזיכרון בלבד: + +- זה מתאים ל-local single-user +- אין כרגע persistence ל-restart של השרת +- decisions יאבדו אם ה-process נופל + +## Frontend flow + +ה-UI נבנה סביב עיקרון של "מינימום לחשוב": + +### מסך התחלה + +- הזנת roots בשדות נפרדים +- בחירה מרובת תיקיות באותו דו-שיח native עבור roots +- בחירת root מועדף מתוך הרשימה הקיימת +- כפתורי הוספה/הסרה לשדות roots +- בחירת תיקיות native ב-Electron +- כפתור ניתוח אחד +- section מתקדם נסתר כברירת מחדל +- `force_rescan` נשאר זמין מתוך section ההגדרות +- `Full Hash Scan` כבוי כברירת מחדל, וניתן להפעיל אותו רק מתוך ה-advanced drawer כאשר נדרשת השוואה מדויקת יותר על חשבון זמן סריקה + +### מסך review + +- ברירת מחדל: tab של `בטוח למחיקה` +- כל cluster מוצג כ-card +- הפעולה הראשית היא בחירת keeper +- סימון פרטני של כל עותק למחיקה או להשארה +- פתיחת תיקיות ומחיקה בודדת זמינות ישירות מכל כרטיס עותק +- הממשק נשאר intentionally compact: שורת כלים קצרה מציגה סטטוס cluster, keeper נוכחי, ספירת מחיקה ופעולות מעבר/מחיקה, בלי שכבות summary נוספות +- כל עותק מציג עטיפת אלבום קטנה כאשר קיימת, ו-fallback קצר כאשר אין עטיפה זמינה +- כל שיר נשאר ניתן להשמעה ישירה מתוך העותק שלו, עם כפתור icon-only ונגן צף יחיד שמופיע רק בזמן השמעה +- אזור ההשוואה כולו נשאר scrollable גם בקבוצות ארוכות, וטבלאות השירים שומרות על גלילה פנימית תקינה בלי לנתק את המשתמש מה־workspace + +### side panel + +מציג: + +- שכבת שקיפות scoring גלויה עם: + - `algorithmic_score` + - `ml_score` + - `base_score` + - `gemini_score` + - `final_score` +- השוואת score ממוקדת בין ה-keeper הפעיל לבין כל עותק אחר ב-cluster +- סיבות הסיווג +- breakdown של scores +- רשימות שירים +- מידע על האלבומים בקבוצה +- מגירת `advanced details` למשתמשים מתקדמים עם פירוט מלא לכל `pair`, כולל `reason_codes` ו-`technical_summary` + +ה־implementation הנוכחי מבוסס על רכיבי `Ant Design` כמו `Collapse`, `Card`, `Segmented`, `Table`, `Result`, `Progress`, ו-`Statistic`, עם overrides ממוקדים כדי לשמור על שפת desktop ולא מראה web generic. +בפרט, `DiffWorkspace` משתמש כעת ב-`Badge.Ribbon`, `Collapse`, `Segmented`, `Flex`, `sticky table header`, ו-`Table.Summary` כדי להציג decision flow ברור יותר בין keeper, המלצת מערכת, מצב הקבוצה, ורמת הכיסוי של tracklist בכל עותק, בלי להעמיס שכבות sticky חופפות. + +### delete confirmation + +הפך למסך finalize עצמאי: + +- כמה תיקיות ממתינות להעברה כרגע +- מי נשמר בכל cluster +- אילו תיקיות כבר הועברו בשלבים קודמים +- האם cluster בוצע חלקית או דורש טיפול +- כפתור ביצוע אחד מפורש לאחר preview מלא +- לפני ביצוע בפועל מוצג אישור קצר שמבהיר שהפעולה תשלח את התיקיות ל-`Recycle Bin` בלבד + +## הרצה מקומית + +### Backend בלבד + +```bash +cd album_deduplicator +uvicorn api.app:app --reload +``` + +### Desktop + +```bash +cd album_deduplicator/frontend +npm run dev:electron +``` + +### Frontend בלבד + +```bash +cd album_deduplicator/frontend +npm run dev +``` + +### CLI + +```bash +cd album_deduplicator +python main.py "C:/Music" "D:/Archive" -p "C:/Music" +``` + +## בדיקות + +### Backend + +```bash +pytest tests -q +``` + +מכסה: + +- `ScoringService` +- `RecommendationService` +- `FastAPI` session flow + +### Frontend + +```bash +cd frontend +npm test +npm run build +``` + +### Desktop + +```bash +cd frontend +npm run dist:desktop +``` + +## מגבלות ידועות + +- אין persistence ל-session store מעבר לחיי השרת. +- אין תמיכה ב-v1 בזיהוי אלבומים עם מספר שירים שונה. +- אין כרגע auth או multi-user isolation, כי המוצר מיועד local single-user. +- חבילת `Electron` עדיין מניחה קיום `Python` מקומי כאשר backend ארוז כ-source resources; אריזת backend ל-executable היא הרחבה טבעית לשלב הבא. + +## כיווני הרחבה טבעיים + +- persistence ל-session state על הדיסק +- background job manager מסודר במקום threads +- תמיכה ב-near-duplicates עם file counts שונים +- diff חזותי חכם בין tracklists +- הרחבת בדיקות e2e מול fixture directories אמיתיים diff --git a/album_deduplicator/docs/ISSUES.md b/album_deduplicator/docs/ISSUES.md new file mode 100644 index 0000000..8538bad --- /dev/null +++ b/album_deduplicator/docs/ISSUES.md @@ -0,0 +1,63 @@ +## תכונות לשיפור והרחבה +[ ] לטפל בבטחה בסריקת תיקיות אלבום שמכילות תתי־תיקיות נלוות כמו Artwork, Scans או Lyrics. בשלב זה תיקיות עם תתי־תיקיות מדולגות כדי למנוע מחיקה של עץ תיקיות שלם דרך `send2trash`. פתרון עתידי צריך להבחין בין תיקיות נלוות לבין אלבומי משנה, לסמן סיכון ב־preview, ולחסום או לאשר במפורש מחיקה של תיקייה שמכילה תתי־תיקיות. +[x] להוסיף אפשרות לפתיחה מיידית של התיקיה המושווית באקספלורר כדי להקל על ההשוואה הידנית למשתמש +[x] לאפשר למחוק תיקיה ספציפית בנפרד מיידית/לסמן תיקיות למחיקה +[x] שיפור מקיף של העיצוב כדי שיהיה מקצועי ונוח יותר +[x] שים דגש על ה-UX הטוב והפשוט ביותר! +[x] לאפשר הוספת תיקיות באמצעות תיבת דו שיח בחירת תיקיה +[x] לאפשר הוספת כמה תיקיות בצורה ברורה יותר למשתמש בשדות נפרדים ולא בשדה טקסט יחיד +[x] יש לאפשר גלילה חלקה ומלאה של התוכן. כרגע התוכן "תקוע" ולא ניתן לגלול בו בצורה תקינה! + +[x] יש להציג את שמות האלבומים/תיקיות הזהים ברשימת התוצאות המקוצרת, במקום ניסוח גנרי כמו "נמצאו 3 עותקים כמעט זהים" +[x] תחת טבלת רשימת שירים והבדלים יש להציג נתונים עיקריים, כמו שם, כותרת, גודל, אורך קובץ וכו' של כל פריט לפי מיקום! +[x] יש להחליף את הניסוח "ביטרייט" ו"איכות משוקללת", "נפח" וכו' לניסוח ידידותי יותר למשתמש דובר העברית הלא טכני +[x] תקן את הניסוח "הגדר כשומר"/"נבחר כשומר" לניסוח תקני בעברית +[x] יש להוסיף כפתור חזרה להתחלה/תפריט ראשי דינמי כדי לאפשר להתחיל סריקה חדשה וכדומה +[x] כפתור "סמן למחיקה" לא עובד בפועל בתיקיות לסקירה, יש לתקן זאת + + +[x] יש להפוך את האבחנה של הנתונים בטבלת ההבדלים לברורה יותר למשתמש, למשל לסמן בצבע תואם מידע זהה/שונה +[x] יש לקצר את טקסט המשנה ברשימת התוצאות המקוצרת, אין צורך לפרט מה מומלץ לשמור! +[x] יש לשנות את הניסוח "טופל" לניסוח מתאים יותר כמו "בטוח למחיקה" וכדומה +[x] ישנה בעיה בטעינת התצוגה כאשר גוללים את תוצאות הסריקה של אלבום כלשהו. לפעמים קטעי האלבומים המציגים את שתי האלבומים השונים פשוט נתקעים בתצוגה +[x] יש להשתמש בכותרת הראשית Music Automatic עבור התוכנה ולהפוך את דף הבית למרשים ויחודי יותר + + +## שלב 2 +[x] יש להוסיף אנימציה משופרת וטקסט תואם בשלב הסריקה +[x] יש להוסיף אפשרות להשמעת השיר ישירות מהממשק לצורך השוואה קלה +[x] יש להוסיף תצוגה מקדימה של תמונת האלבום להשוואת נוספת! +[x] יש ליישר את שלב 1 של תצוגת הסריקה ואת שלב 3 של מרכז ההעברה וההשוואה לשפה חזותית עקבית יותר עם שלב 2, תוך עיצוב קומפקטי, פשוט ונקי יותר + +### הושלם +- מסך הסריקה עבר ל-rows נפרדים לכל root, עם הוספה/הסרה ברורה ובחירת תיקיות native ב-Electron. +- סביבת ה-diff תומכת כעת בפתיחה ישירה של כל תיקייה, סימון פרטני למחיקה, ומחיקה מיידית של עותק בודד מתוך ה-cluster. +- ה-layout וה-scroll של כל הפאנלים הוקשחו כך שהתוכן נגלל באופן מלא ויציב גם במסכים צרים וגם כאשר פס ה-preview התחתון פתוח. +- רשימת הקבוצות המקוצרת מציגה כעת שמות אלבומים/תיקיות בפועל, עם כותרות וסיכומי משנה ברורים יותר. +- אזור ההשוואה מציג טבלת שירים עשירה לפי עותק, כולל שם קובץ, אורך, גודל וקצב נתונים לכל מיקום. +- נוסחי המדדים והפעולות עודכנו לעברית ידידותית ותקנית יותר, כולל "בחר לשמירה"/"נבחר לשמירה". +- נוספו כפתורי ניווט מהירים להתחלה מחדש ממסך הסיכום ומתצוגת ה-review. +- תיקיות בלשונית "לסקירה" יכולות כעת להישלח למחיקה לפי ההמלצה הראשונית, והזרימה מכוסה בטסט frontend. +- טבלת ההבדלים מסמנת כעת באופן עקבי שדות זהים, שונים וחסרים באמצעות legend, badge-ים וצבעים תואמים. +- טבלת ההשוואה קיבלה בורר `Segmented` לבחירת בסיס ההשוואה, יחד עם מצב אוטומטי/ידני ברור יותר סביב ה-keeper הפעיל. +- רשימת הקבוצות המקוצרת קיבלה טקסט משנה קצר ומעשי יותר, וסטטוסים ברורים כמו "בטוח למחיקה" ו-"ממתין לבדיקה". +- מעבר בין קבוצות ב-review מאפס את סביבת ההשוואה ומקשיח את ה-layout/scroll כך שכרטיסי האלבומים אינם "נתקעים" בזמן גלילה. +- מסך הבית עוצב מחדש סביב המותג Music Automatic, עם hero ייחודי, שפה חזותית חדשה וזרימת setup ברורה יותר. +- מסך הסריקה קיבל strip ברור של שלבי העבודה, אנימציית progress חיה יותר, ו-copy דינמי שמותאם לשלב הפעיל בפועל. +- מסך הסריקה ומרכז ההעברה וההשוואה יושרו כעת למבנה קומפקטי ועקבי יותר עם מסך הסיכום: כרטיס פתיחה קצר, grid KPI ברור, ופחות "hero sections" כבדים. +- סביבת ה-review מאפשרת כעת לנגן שירים ישירות מתוך טבלת ההשוואה, עם נגן docked שמציג מאיזה עותק מתנגן הקטע הפעיל. +- נגן ההשוואה ב-review עודכן לבאנר צף אמיתי שמופיע רק בזמן השמעה, עם mini-player קומפקטי וציר זמן ברור ונגיש יותר. +- כל כרטיס אלבום ב-review מציג כעת עטיפת אלבום להגדלה כאשר קיימת עטיפה חיצונית או embedded art זמין דרך ה-API. +- באנר "שלב ההעברה הסופי מוכן" קיבל פריסה חזקה יותר של מצב, אמון ו-CTA, כך שהוא משתלב טוב יותר עם שאר שפת ה-desktop. + +### בדיקת רגרסיה 2026-03-08 +- תוקנה רגרסיית UX שבה המחיקה הבודדת נשארה זמינה גם ללא `keeper` פעיל, למרות שה-API חוסם זאת. הכפתור וקיצור המקלדת `D` נחסמו עד לבחירת עותק לשמירה. +- נוסח כפתור בחירת התיקיות ב-Desktop יושר לזרימת בחירה מרובת תיקיות, כדי שלא יוצג בטעות כפעולת `Browser`. +- טסטי `App` ו-`DiffWorkspace` עודכנו כך שישקפו את מסך ה-setup החדש, ה-summary החדש, והגנת המחיקה הבודדת ללא `keeper`. +- נבדק המצב שבו ב-`Browser fallback` אין chooser מלא לבחירת כמה תיקיות. המקרה לא סווג כרגרסיה בשלב זה, משום שהמוצר וה-docs הועברו רשמית לזרימת `Electron` desktop-first וה-fallback הידני עדיין זמין. + +### בדיקת רגרסיה 2026-03-10 +- תוקנה רגרסיה ב-`review` שבה `recommended_keeper_id` הוצג למשתמש כאילו כבר נבחר ידנית לשמירה, למרות שהקבוצה עדיין לא אושרה. +- לחיצה על אותו עותק מומלץ ב-`review` יוצרת כעת `delete_folder_ids` מלאים ושולחת אותם ל-API, כך שהפריט נכנס ל-`delete preview`, למרכז ההעברה, ולמונה של כפתור `העבר למחזור`. +- נוספו טסטים ממוקדים ל-`DiffWorkspace` ול-`useDeduplicator` כדי לקבע גם את ההבחנה ב-UX וגם את זרימת ה-preview לאחר בחירה ידנית. +- נוספה כעת גם פעולת ביטול הכרעה ברמת cluster: המשתמש יכול לבחור `שמור את כל העותקים` מתוך ה-`review` או מתוך מסך ה-`finalize`, וה-preview/מוני המחיקה מתעדכנים מיידית בהתאם. diff --git a/album_deduplicator/docs/ML_VALIDATION.md b/album_deduplicator/docs/ML_VALIDATION.md new file mode 100644 index 0000000..ce9fe71 --- /dev/null +++ b/album_deduplicator/docs/ML_VALIDATION.md @@ -0,0 +1,283 @@ +# ולידציית מודל ML וקביעת סף למחיקה בטוחה + +מסמך זה מרכז את מצב הדברים הנוכחי סביב מודל ה-`ML` ב-`Album Deduplicator`, את הממצאים הראשוניים שעלו מבדיקת סט הנתונים הקיים, ואת הדרך המקצועית לקבוע האם ניתן להגדיר סף שמעליו אפשר לסמן אלבומים כ-`בטוח למחיקה`. + +## מטרת הבדיקה + +השאלה העסקית אינה רק: + +- "עד כמה שני אלבומים דומים?" + +אלא בפועל: + +- "מאיזה ציון אפשר לקבוע ברמת ביטחון קיצונית שמדובר באותו אלבום, כך שניתן להציע מחיקה אוטומטית בבטחה?" + +זו בעיית `safe delete`, ולא רק בעיית `similarity`. + +## מצב המערכת כיום + +במוצר עצמו: + +- `base_score = 0.35 * algorithmic_score + 0.65 * ml_score` +- `Gemini` רץ רק לזוגות גבוליים +- `safe delete` נקבע כיום לפי `final_score > 90` או `identical_by_hash` + +מקורות רלוונטיים: + +- [config.py](c:/Users/me/Documents/GitHub/Music-Automatic/album_deduplicator/music_dup_lib/config.py) +- [scoring_service.py](c:/Users/me/Documents/GitHub/Music-Automatic/album_deduplicator/music_dup_lib/services/scoring_service.py) +- [recommendation_service.py](c:/Users/me/Documents/GitHub/Music-Automatic/album_deduplicator/music_dup_lib/services/recommendation_service.py) + +## ממצאים ראשוניים מהדאטה הקיים + +בוצעה בדיקה על: + +- `similarity_model/data/album_pair_features_test.csv` +- המודל המאומן `similarity_model/models/lgbm_regressor_model.joblib` + +הבדיקה בחנה את `ml_score` לבדו מול ה-labels שכבר קיימים בסט הבדיקה. + +### תוצאות לפי סף על `ml_score` + +לפי הגדרת "חיובי" הנוכחית בסט: + +- `combined_high_certainty` +- `gemini_cached` + +התקבלו התוצאות הבאות: + +| סף `ml_score` | זוגות שנבחרו | Precision | False Positives | +|---|---:|---:|---:| +| `>= 90` | 221 | `98.19%` | 4 | +| `>= 95` | 170 | `98.82%` | 2 | +| `>= 97` | 100 | `99.00%` | 1 | +| `>= 98` | 16 | `100.00%` | 0 | +| `>= 99` | 2 | `100.00%` | 0 | + +### מסקנה ראשונית + +אם בוחנים רק את סט הבדיקה הקיים ורק את התוויות הקיימות: + +- הסף הראשון שבו התקבלו `0` false positives הוא `ml_score >= 98` + +אבל זו אינה הוכחה מספקת לכך ש-`98` הוא סף מחיקה בטוח בפרודקשן. + +## למה אי אפשר עדיין להכריז על "ודאות גמורה" + +### 1. ה-ML מחזיר ציון דמיון, לא הסתברות מכוילת + +המודל הנוכחי מנבא `similarity score` בטווח `0-100`. + +לכן: + +- `98` אינו אומר "98% סיכוי שזה אותו אלבום" +- `100` אינו אומר "ודאות מוחלטת" + +כדי לתרגם ציון לסיכון מחיקה, צריך לבצע `calibration` או למדוד אותו אמפירית על סט ולידציה אמיתי. + +### 2. ה-labels הנוכחיים אינם gold truth ידני + +ב-`similarity_model/src/data_preparation.py` חלק מה-labels נוצרים מ: + +- יוריסטיקות עם ספים קשיחים +- `Gemini` +- שילוב ציונים פנימי + +כלומר, סט ההערכה הנוכחי אינו אמת-מידה ידנית נקייה לחלוטין. + +### 3. יש חוסר התאמה בין חישוב הסקור בדאטה-פרפ לבין חישוב הסקור במוצר + +בבניית הדאטה מופיע חישוב: + +- `0.7 algorithmic + 0.3 ml` + +ואילו במערכת עצמה מוגדר: + +- `0.35 algorithmic + 0.65 ml` + +לכן, גם אם נמצא threshold טוב על הדאטה הקיים, הוא לא בהכרח משקף במדויק את התנהגות המוצר בפועל. + +מקורות: + +- [data_preparation.py](c:/Users/me/Documents/GitHub/Music-Automatic/similarity_model/src/data_preparation.py) +- [config.py](c:/Users/me/Documents/GitHub/Music-Automatic/album_deduplicator/music_dup_lib/config.py) + +### 4. יש סיכון ל-leakage ב-train/test split + +הפיצול הנוכחי נעשה רנדומית ברמת שורות: + +- `train_test_split(..., shuffle=True)` + +בבדיקה על הנתונים הקיימים נמצאה חפיפה משמעותית של תיקיות בין train ל-test. + +המשמעות: + +- ייתכן שהמודל נבחן על זוגות שקשורים לאלבומים שכבר הופיעו אצלו באימון +- זה עלול לנפח את המדדים כלפי מעלה + +## דוגמה נגדית חשובה + +בסט הבדיקה נמצא לפחות מקרה אחד שבו: + +- `ml_score >= 97` +- אבל ה-label היה שלילי/גבולי (`hard_negative_gemini_cached`, ציון מטרה `65`) + +כלומר: + +- גם `97` אינו סף נקי לחלוטין לפי הדאטה הנוכחי + +זו בדיוק הסיבה שלא נכון לקבוע מדיניות מחיקה אוטומטית רק לפי ציון ML בודד. + +## ההגדרה המקצועית הנכונה לבעיה + +במקום למדוד רק `similarity regression`, צריך להגדיר בעיית החלטה ברורה: + +### מחלקות מומלצות + +- `safe_delete` +- `same_album_but_review` +- `different` + +המשמעות העסקית: + +- `safe_delete`: אפשר להציע מחיקה אוטומטית +- `same_album_but_review`: כנראה אותו אלבום, אבל עדיין דורש אישור אנושי +- `different`: אסור להציע מחיקה + +## איך לבצע את הבדיקה בצורה מקצועית + +### שלב 1: לבנות Gold Validation Set + +יש ליצור סט ידני של כמה מאות זוגות לפחות, עם תיוג אנושי ברור. + +מומלץ לדגום במיוחד: + +- זוגות עם `ml_score` גבוה +- זוגות עם `final_score` גבוה +- זוגות עם `algorithmic_score` נמוך אבל `ml_score` גבוה +- זוגות באזורי הסף: `85-90`, `90-95`, `95-98`, `98+` + +לכל זוג צריך לקבוע ידנית: + +- האם זה אותו אלבום +- האם זה מספיק בטוח למחיקה אוטומטית +- האם יש חריגות שמחייבות review גם אם האלבומים זהים + +### שלב 2: לפצל לפי קבוצות, לא לפי שורות + +הפיצול צריך להיות לפי ישות אלבום/משפחה ולא לפי pair בודד. + +מטרה: + +- למנוע מצב שבו אותו אלבום מופיע גם ב-train וגם ב-test דרך זוגות שונים + +### שלב 3: למדוד Threshold Tables + +עבור כל אחד מהציונים: + +- `ml_score` +- `base_score` +- `final_score` + +צריך לחשב לכל threshold: + +- `precision` +- `recall` +- `false positives` +- `selected pairs` +- `false positive rate` + +בפועל, לשאלת `safe delete`, המדד המרכזי הוא: + +- `precision` + +אבל חובה להסתכל גם על: + +- גודל המדגם שמעל הסף + +### שלב 4: לחשב רווחי סמך + +גם אם בסף מסוים התקבל: + +- `0` false positives + +זה עדיין לא אומר שהסף "מוכח". + +אם רק `16` זוגות עברו את הסף, אין כאן מספיק evidence כדי לקבוע ודאות מבצעית. + +לכן יש למדוד: + +- `Wilson lower bound` +- או `Clopper-Pearson interval` + +כדי להבין מהו הגבול התחתון הסביר של ה-precision האמיתי. + +### שלב 5: לבחור סף עסקי ולא רק סטטיסטי + +מחיקה היא פעולה רגישה, לכן אין הצדקה להסתמך על ML בלבד. + +המלצה מקצועית: + +- לא להשתמש ב-`ml_score` בלבד כדי לקבוע `safe delete` +- להשתמש ב-`final_score` יחד עם guardrails דטרמיניסטיים + +## Guardrails מומלצים למחיקה בטוחה + +גם אם נמצא threshold איכותי, מומלץ לדרוש בנוסף: + +- אותו מספר קבצי מוזיקה +- `keeper` יחיד וברור +- התאמה חזקה ברשימת השירים +- ואם מדובר במקרה גבולי אך חשוב, `Full Hash Scan` +- `Gemini` לכל היותר כתמיכה, לא כמקור יחיד למחיקה + +במילים אחרות: + +- `ML` צריך להיות רכיב תומך בהחלטה +- לא אישור מחיקה בלעדי + +## המלצה אופרטיבית למוצר + +עד שלא ייבנה `gold validation set` אמיתי: + +- לא לקבוע "ודאות גמורה" לפי `80`, `90` או `95` +- לא להקשיח מדיניות מחיקה אוטומטית על סמך `ml_score` בלבד + +אם חייבים סף ראשוני שמרני לניסוי פנימי בלבד: + +- `ml_score >= 98` הוא הסף הראשון ללא false positives בסט הבדיקה הנוכחי + +אבל גם במקרה זה: + +- יש להתייחס אליו כ-`working hypothesis` +- לא כ-proof למחיקה בטוחה בפרודקשן + +## פערים ידועים שיש לטפל בהם לפני קיבוע מדיניות + +- ליישר את חישוב ה-blend בין `similarity_model` לבין `album_deduplicator` +- לבנות סט ולידציה ידני ייעודי ל-`safe delete` +- לבצע split ללא leakage +- למדוד את `base_score` ו-`final_score`, לא רק את `ml_score` + +## איסוף feedback ממשתמשים בפועל + +נוסף מנגנון איסוף החלטות משתמשים תחת `album_deduplicator`, כדי להתחיל לבנות evidence אמיתי מהשימוש במוצר: + +- מחיקה מוצלחת לסל המחזור נרשמת כ-`same_album_confirmed` עם `evidence_strength=strong`. +- החלטות זמניות כמו `שמור את כל העותקים`, שינוי keeper או סימון מחיקה אינן נרשמות ל-dataset לפני מחיקה בפועל. +- כל אירוע נשמר כ-`JSONL` וכולל את ציוני ה-pairs ואת מדיניות ה-scoring שהייתה בתוקף בזמן ההחלטה. + +תיעוד הפורמט והמיקום נמצא ב-[USER_FEEDBACK_DATASET.md](USER_FEEDBACK_DATASET.md). +- לבחון במיוחד מקרים של `algo נמוך + ml גבוה` + +## שאלת העבודה הנכונה להמשך + +במקום לשאול: + +- "מאיזה אחוז דמיון ה-ML צודק?" + +עדיף לשאול: + +- "מאיזה threshold, ובאילו guardrails נוספים, הסיכון ל-false positive מספיק נמוך כדי לאפשר safe delete?" + +זו השאלה שצריכה להנחות את הוולידציה העתידית ואת מדיניות ה-UI. diff --git a/album_deduplicator/docs/Notes.txt b/album_deduplicator/docs/Notes.txt new file mode 100644 index 0000000..cf8af7f --- /dev/null +++ b/album_deduplicator/docs/Notes.txt @@ -0,0 +1,5 @@ +## מצב נוכחי עבור find_duplic_albums: + +- מיזוג התיקיות לא עובד כמצופה. הוא מוסיף מטאדאטה חסר אבל תמונת האלבום לא מתמזגת +וכן גם לא קיימת עדיפות למטדאטה בעברית על מטאדאטה באנגלית ועל אמיתית לעומת גנרית (כמו "artist" או "unknown artist") +ניתן לשלב את מודל ה-AI שלי לזיהוי שמות גנריים של אלבום או זמר \ No newline at end of file diff --git a/album_deduplicator/docs/USER_FEEDBACK_DATASET.md b/album_deduplicator/docs/USER_FEEDBACK_DATASET.md new file mode 100644 index 0000000..120658b --- /dev/null +++ b/album_deduplicator/docs/USER_FEEDBACK_DATASET.md @@ -0,0 +1,90 @@ +# User Feedback Dataset + +מסמך זה מתאר את מנגנון איסוף נתוני ההחלטות של משתמשים לצורך אימון ושיפור עתידי של מודל ה-ML של `Album Deduplicator`. + +## מטרת המנגנון + +המערכת אוספת רק החלטות שנעשו בפועל בתוך זרימת העבודה של המשתמש: + +- מחיקה מוצלחת לסל המחזור נחשבת evidence חזק לכך שהעותק שנמחק והעותק שנשמר הם אותו אלבום מבחינת המשתמש. +- בחירה `שמור את כל העותקים`, שינוי keeper וסימוני מחיקה זמניים אינם נרשמים ל-dataset, כי המשתמש יכול לשנות את ההחלטה לפני הפעולה הסופית. +- רק מחיקה שבוצעה בפועל ונרשמה כהצלחה נחשבת gold label לאימון. + +המטרה היא לבנות לאורך זמן קובץ training feedback נקי יותר מה-labels ההיוריסטיים ההיסטוריים. + +## מיקום הקובץ + +ברירת המחדל בהתקנת Windows רגילה: + +```text +%LOCALAPPDATA%\Music Automatic\Album Deduplicator\user_feedback\user_feedback_events.jsonl +``` + +אפשר לשנות את תיקיית הבסיס באמצעות משתנה סביבה: + +```text +ALBUM_DEDUP_USER_DATA_DIR +``` + +כאשר המשתנה מוגדר, הקובץ יישמר תחת: + +```text +\user_feedback\user_feedback_events.jsonl +``` + +הבחירה בנתיב user-data נועדה לעבוד גם בהתקנות ארוזות שבהן תיקיית האפליקציה עצמה יכולה להיות read-only. + +## פורמט + +הקובץ הוא `JSONL`/`NDJSON`: כל שורה היא אירוע עצמאי עם `schema_version`. + +שדות מרכזיים: + +- `schema_version`: גרסת הסכמה. כרגע `1.0`. +- `event_type`: כרגע `delete_executed`. +- `label`: כרגע `same_album_confirmed`. +- `evidence_strength`: כרגע `strong`, כי נשמרות רק מחיקות מוצלחות בפועל. +- `session_id`: מזהה session מקומי. +- `cluster`: metadata של הקבוצה, כולל `cluster_id`, `folder_ids`, `pair_ids`, `confidence_bucket`, ו-`reason_codes`. +- `keeper`: העותק שנשמר. +- `target`: העותק שעליו המשתמש פעל. +- `pairs`: נתוני scoring של הזוגות הרלוונטיים, כולל `algorithmic_score`, `ml_score`, `base_score`, `final_score`, `is_identical_by_hash`, ו-`similarity_scores`. +- `model_policy`: משקלי scoring וספי review/safe שהיו בתוקף בזמן ההחלטה. + +## יצוא מהממשק + +במסך `העברה` מוצג panel קטן עם מספר אירועי האימון שנאספו, כפתור `יצא נתונים לשיתוף`, וכפתור `נקה היסטוריה`. + +ביישום ה-Desktop, הכפתור פותח חלון `Save As` מקומי ושומר את הקובץ דרך מעטפת `Electron`, כדי שלא ייפתח חלון דפדפן פנימי ריק. בהרצת browser fallback, הכפתור משתמש בהורדת קובץ רגילה. + +שם ברירת המחדל של קובץ היצוא כולל זמן, משתמש, מכונה ומזהה קצר כדי להבדיל בין סריקות ועמדות שונות: + +```text +ma-feedback_20260515-123456_me-user-studio-pc_a1b2c3.jsonl +``` + +הכפתור מוריד את קובץ ה-JSONL דרך: + +```text +GET /api/ml-feedback/export +``` + +אפשר לבדוק סטטוס דרך: + +```text +GET /api/ml-feedback/summary +``` + +אפשר למחוק את היסטוריית הזיהויים המקומית דרך הממשק או דרך: + +```text +DELETE /api/ml-feedback +``` + +## שימוש עתידי לאימון + +בשלב הבא מומלץ להוסיף כלי export שממיר את ה-JSONL ל-dataset עבור `similarity_model`: + +- `same_album_confirmed` יכול לשמש positive label ברמת pair. +- יש לפצל train/test לפי family או cluster, לא לפי שורות, כדי למנוע leakage. +- יש לשמור את `schema_version` ואת `model_policy` כדי למדוד drift ולשחזר איך כל החלטה נוצרה. diff --git a/album_deduplicator/docs/UX_IMPROVEMENT_PLAN.md b/album_deduplicator/docs/UX_IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..1e8c5ba --- /dev/null +++ b/album_deduplicator/docs/UX_IMPROVEMENT_PLAN.md @@ -0,0 +1,85 @@ +# תוכנית שיפור חוויית משתמש (UX/UI) - Album Deduplicator + +מסמך זה מתאר את החזון והתוכנית האופרטיבית להפיכת המערכת לאפליקציה בעלת חוויית משתמש ברמה של מוצרי פרימיום שולחניים (בדומה ל-CleanMyMac, GitHub Desktop, ו-Lightroom), תוך יישום עקרונות "Don't Make Me Think". + +## 1. מטרות העל של ה-UX + +* **בניית אמון (Trust):** מחיקת קבצים היא פעולה שיוצרת חרדה. על המערכת לשקף ודאות אבטחה ("שום דבר לא נמחק לצמיתות, הכל עובר לסל המיחזור") ולהסביר את החלטותיה בשפה פשוטה ואנושית, לא רק במספרים. +* **הפחתת עומס קוגניטיבי:** הסתרת מידע טכני (ציוני סף, אלגוריתמיקה) מאחורי תצוגה נקייה, והבלטתו רק למי שמעוניין ("Progressive Disclosure"). +* **זרימה רציפה (Flow):** מתן אפשרות למשתמש לעבור על עשרות החלטות במהירות באמצעות ממשק חכם, השוואה ויזואלית ברורה, וקיצורי מקלדת. + +## 2. השראות עיצוביות ותפיסתיות + +* **CleanMyMac X:** לסריקה מרתקת, פידבק ויזואלי מצוין, ותחושת ביטחון במסך התוצאות והאישור. +* **כלי השוואת קוד (Git Diff):** להצגת מסך ה"השוואה" בין שני אלבומים - הדגשת **השוני** (ביטרייט גבוה יותר, שיר חסר) במקום הצגת רשימות טקסט זהות ומשעממות. +* **Lightroom Culling:** לזרימת עבודה מהירה בעזרת מקלדת לבחירת התוצאה הטובה ביותר מבין כמה אפשרויות דומות. + +## 3. פירוט התוכנית לפי שלבים (User Journey) + +### שלב א': מסך הפתיחה והסריקה +* **מינימליזם:** המסך הראשי יכיל רק בחירת תיקיית מקור, תיקיית ארכיון (אופציונלי), ותיקייה מועדפת לשמירה. +* **Workflow Rail גלובלי:** בראש חלון ה-desktop יופיע rail קבוע בין `בחירה`, `סריקה`, `סיכום`, `סקירה`, ו-`העברה`, כך שאפשר לחזור לכל שלב זמין בלי לחפש כפתורי חזרה מפוזרים. +* **הסתרת מורכבות:** כל הגדרות ה-ML, Gemini, והביטרייט יוסתרו תחת כפתור "הגדרות סריקה מתקדמות" (Gear icon). +* **סריקה חיה (Active Scanning Progress):** + * במקום רק מד אחוזים, יופיע טקסט מתחלף שמסביר מה קורה מאחורי הקלעים: *"סורק 1,500 קבצי אודיו..."*, *"מנתח חתימות שמע..."*, *"נעזר בבינה מלאכותית להשוואת מקרים גבוליים..."*. זה בונה תחושת ערך ואמון שהמערכת עובדת קשה עבור המשתמש. + +### שלב ב': תצוגת התוצאות (The Workspace) +המסך הראשי יחולק לאזור רשימת ה-Clusters (הקבוצות) ול-Side Panel מרכזי ועשיר. + +#### רשימת ה-Clusters (צד ימין/שמאל בהתאם ל-RTL): +* כל Cluster יוצג כ"כרטיסיה" חכמה. +* בכרטיסיה יהיה כתוב בשפה פשוטה הסטטוס: + * 🟢 **בטוח למחיקה:** "נמצאו 2 עותקים זהים לחלוטין. מומלץ לשמור את העותק בתיקיית Master." + * 🟡 **דורש סקירה:** "שירים זהים, אך באיכות שמע שונה. האם לשמור את האיכות הגבוהה?" +* אינדיקטורים חזותיים מהירים: איזה אלבום הוגדר כ-Keeper (הדגשה ירוקה) ואילו מסומנים למחיקה (אדום דהוי עם סמל פח). + +#### לוח ההשוואה (The Diffing Side Panel): +זה הלב של חוויית המשתמש, המקום שבו מתקבלת ההחלטה. +* **מבט זה לצד זה (Side-by-Side):** עטיפת האלבום, נתיב, ומטא-דאטה. +* **הבלטת הבדלים (Visual Diffs):** + * אם לאלבום א' יש 320kbps ולאלבום ב' 192kbps - המספר 320 יודגש בירוק, ו-192 באדום. + * רשימת השירים תוצג רק פעם אחת, תוך סימון שירים שקיימים רק באחד האלבומים. +* **סיכום אנושי (AI/System Summary):** טקסט קריא שאומר: *"האלבומים זהים ב-98%. אלבום א' נבחר כמומלץ כי הוא נמצא בתיקיית היעד המועדפת וכולל עטיפת אלבום."* טקסט נסתר (Tooltip) יציג את הניקוד המתמטי המדויק. +* **פעולות מהירות (Quick Actions):** + * [חדש] **אייקון תיקייה:** "פתח באקספלורר" ליד כל נתיב אלבום, כדי שהמשתמש יוכל לבדוק בעצמו בשנייה. + * [חדש] **כפתור "מחק עכשיו":** מאפשר לעקוף את זרימת המחיקה המרוכזת ולמחוק תיקייה ספציפית מיידית לסל המיחזור תוך כדי סקירה. + * כפתורי רדיו גדולים וברורים לבחירת ה-Keeper. + +### שלב ג': מקלדת וזרימת עבודה (Power Features) +* **תמיכה במקלדת בלבד:** + * `חצים למעלה / למטה`: מעבר ל-Cluster הבא/קודם. + * `חצים ימינה / שמאלה`: שינוי ה-Keeper הנבחר בתוך ה-Cluster. + * `Space`: סימון ה-Cluster כ"דלג" (Skip). + * `Enter`: מעבר למסך אישור מחיקה מרוכז. + +### שלב ד': מסך אישור וסיום (The "Clean" Moment) +* **Preview מרוכז ומרגיע:** + * "פעולה זו תעביר 14 תיקיות (סה"כ 3.2GB) אל סל המיחזור." + * "אף קובץ לא יימחק לצמיתות בשלב זה." + * הצגת רשימה מתגלגלת ונקייה של מה שנמחק מול מה שנשמר. + * המסך יציג גם מצב חלקי: מה כבר הועבר, מה עדיין ממתין, ואילו קבוצות בוצעו רק חלקית. +* **מסך "ניצחון" (Success Screen):** + * לאחר המחיקה, הצגת מסך מסכם עם אנימציה קלה: "סיימנו! חסכת 3.2GB של מקום אחסון והספרייה שלך נקייה יותר." + +## 4. משימות לפיתוח (Frontend - React + CSS) + +- [x] **הטמעת ספריית UI מודרנית:** ה־frontend עבר ל־`Ant Design 6` עם `Happy Work Theme`, RTL מלא, ו־theme tokens בסגנון `Cartoon Desktop` מתון. +- [x] **פיתוח קומפוננטת Visual Diff:** נבנתה תצוגת השוואה רוחבית שמדגישה שדות עדיפים/חלשים בין כל עותקי ה-`AlbumSummary` בקבוצה. +- [x] **שילוב פקודות מערכת מתוך הדפדפן:** + * נוסף Endpoint ב-FastAPI: `POST /api/system/open-explorer` לפתיחת נתיב ב-Windows Explorer. + * נוסף Endpoint למחיקה בודדת: `POST /api/analysis-sessions/{id}/delete-single` לטובת מחיקה מיידית ללא המתנה לסוף. +- [x] **הטמעת אירועי מקלדת (Keyboard Listeners):** קיים Hook שמנהל ניווט בין clusters, בחירת keeper, skip, פתיחה ב-Explorer וסימון למחיקה. +- [x] **עיצוב מחדש של ה-Progress Bar:** מסך הסריקה מציג כעת שלבים ויזואליים עם טקסטים דינמיים מתוך ה-progress events. +- [x] **איחוד אחוזי ההתקדמות לסריקה רציפה אחת:** אחוזי מסך הסריקה וה-`workflow rail` משקפים כעת `0-100` כולל על פני כל תתי-השלבים של הניתוח, עם differentiation צבעוני עדין לפי השלב הפעיל במקום reset של האחוז בכל משימה פנימית, כולל progress פעיל גם בזמן יצירת זוגות ההשוואה לפני שלב ה-scoring. +- [x] **מסך finalize עצמאי למחיקה מרוכזת:** נוסף מסך דסקטופי נפרד עם פירוק מלא של "יימחק / יישמר / כבר הועבר", כולל סטטוס חלקי למחיקה בשלבים. +- [x] **דחיסת סביבת העבודה ושיפור גלילה:** אזור ה-`review` עבר לפריסה קומפקטית יותר בסגנון desktop, עם header דביק, גלילה מלאה למסך, גלילה אופקית יציבה בהשוואת עותקים, וטבלת שירים עם scrolling פנימי יעיל. +- [x] **העמקת היררכיית ההחלטה ב-Review Workspace:** מסך ה-`DiffWorkspace` מציג כעת decision strip ברור עם keeper פעיל, המלצת מערכת, בסיס ההשוואה ומה יימחק; כרטיסי האלבומים משתמשים ב-`Badge.Ribbon` + `Descriptions` להצגת מצב ו-metadata; וטבלת השירים כוללת `sticky header`, `Table.Summary`, והדגשת שורות שונות/חסרות כדי לזרז החלטה. +- [x] **איזון sizing וגלילה בסביבת ה-review:** אזורים רוחביים וארוכים קיבלו `overflow` ו-scrollbars יציבים, עמודות הטבלה והכרטיסים הוצרו למידות desktop סבירות יותר, וטקסטים ארוכים עברו ל-`ellipsis + tooltip` כדי למנוע שבירת layout בנתיבים, כותרות ושמות קבצים. +- [x] **שילוב סמלים עקבי בכל המסכים:** מסכי `setup`, `scanning`, `summary`, `review` ו-`finalize` קיבלו אייקונים עקביים לשיפור היררכיה חזותית ופעולות מהירות בלי להעמיס על המשתמש. +- [x] **אזהרת restart לפני סריקה מחדש:** כאשר קיימות כבר תוצאות סריקה, לחיצה על `התחל סריקה` מציגה confirm ברור שמסביר שהתוצאות והסימונים הקיימים יאופסו ושסריקה חדשה עשויה לקחת זמן. +- [x] **כפתור סגירה אייקוני לנגן ההשוואה:** נגן המדיה הדבוק ב-`review` משתמש כעת ב-`X` במקום כפתור טקסטואלי, כדי לשמור על פס שליטה קומפקטי יותר בסגנון desktop. +- [x] **איחוד מערכת ה־UI למסכי desktop:** מסכי `setup`, `scanning`, `summary`, `review` ו-`finalize` שוכתבו לרכיבי `AntD` (`Card`, `Segmented`, `Collapse`, `Table`, `Result`, `Progress`, `Statistic`) עם שכבת overrides ייעודית שמדגישה playful surfaces, פינות רכות, ו-flow של desktop tool. +- [x] **ייצוב גלילה בין כל ה-views:** לכל מסך ראשי הוגדר scroll container יחיד וברור ברמת ה-shell, וב-`review` הופרדו גלילת ה-sidebar, גלילת ה-workspace הראשי, הגלילה האופקית של כרטיסי האלבומים, וגלילת טבלת השירים כך שלא יחתכו זו את זו. +- [x] **הוספת בקרת בסיס השוואה ב-Review:** טבלת השירים קיבלה `Segmented` לבחירת העותק שמולו מוצגים ההבדלים, `Alert` שמסביר אם הטבלה עוקבת אוטומטית אחרי ה-keeper או מקובעת ידנית, ו-legend קצר שמבהיר מצבי "זהה / שונה / חסר". +- [x] **Workflow rail גלובלי בין שלבים:** נוסף rail עליון, sticky וקומפקטי בין `setup`, `scan`, `summary`, `review` ו-`finalize`, עם קפיצה רק לשלבים הזמינים לפי מצב ה-session. +- [x] **שמירת session בעת חזרה ל-setup:** עריכת שדות הסריקה מתוך `setup` אינה מוחקת את תוצאות הסריקה הקודמות; האיפוס המלא קורה רק בעת התחלת סריקה חדשה בפועל. diff --git a/album_deduplicator/docs/WINDOWS_RELEASES.md b/album_deduplicator/docs/WINDOWS_RELEASES.md new file mode 100644 index 0000000..63fe7c6 --- /dev/null +++ b/album_deduplicator/docs/WINDOWS_RELEASES.md @@ -0,0 +1,98 @@ +# Windows Releases + +מסמך זה מתאר את תהליך ה-`CI/CD` הנוכחי לפריסת אפליקציית ה-`Windows` של `Album Deduplicator`. + +## קבצים רלוונטיים + +- `frontend/package.json` +- `frontend/electron/app-version.cjs` +- `frontend/electron-builder.config.cjs` +- `frontend/scripts/sync-icons.py` +- `.github/workflows/album-deduplicator-windows-release.yml` + +## מה ה-workflow עושה + +ה-workflow משתמש ב-`windows-latest` ומחולק ל-3 מסלולים: + +1. `verify` + - רץ על `pull_request`, `push` ל-`main`, `workflow_dispatch`, וגם לפני release על tag + - מתקין `Python 3.12` ו-`Node 24` + - מריץ `python -m pytest tests -q` + - מריץ `npm test` + - מריץ `npm run build` + +2. `package-preview` + - רץ על `push` ל-`main` ועל `workflow_dispatch` + - בונה מתקין `NSIS` לא חתום עם `npm run dist:desktop` + - שומר את תוצרי האריזה תחת `frontend/desktop-dist` + - מעלה את קובצי ה-`exe`, `blockmap`, ו-`latest.yml` כ-artifact של ה-run + +3. `publish-release` + - רץ רק על tag מהצורה `album-deduplicator-vX.Y.Z` + - מחלץ את גרסת האפליקציה ישירות מתוך ה-tag + - בונה ומפרסם את קובצי ה-`Windows` מתוך `frontend/desktop-dist` ל-`GitHub Releases` דרך `electron-builder` + +## איך משחררים גרסה + +1. ודא שהבדיקות עוברות מקומית: + +```bash +cd album_deduplicator +pytest tests -q + +cd frontend +npm test +npm run sync:icons +npm run dist:desktop +``` + +2. צור tag release: + +```bash +git tag album-deduplicator-v0.1.0 +git push origin album-deduplicator-v0.1.0 +``` + +3. ה-workflow ייצור `GitHub Release` ויצרף אליו את מתקין ה-`Windows` + +במהלך ה-build: + +- `frontend/electron/app-version.cjs` מעדיף את `GITHUB_REF_NAME` או tag מקומי שמצביע ל-`HEAD` +- `electron-builder` מקבל את הגרסה דרך `extraMetadata.version` +- `Electron` מעביר את אותה גרסה גם ל-backend דרך `ALBUM_DEDUP_VERSION` + +## למה נבחרה הגישה הזו + +- `electron-builder` כבר נמצא בפרויקט ומוגדר ל-`NSIS`, כך שאין צורך להוסיף מערכת packaging נוספת +- ה-tag ב-Git הוא כעת מקור האמת לגרסת release, ולכן לא צריך לעדכן ידנית את `frontend/package.json` לפני כל שחרור +- האייקון של האפליקציה נגזר אוטומטית מתוך `album_deduplicator/frontend/build/icons/app-icon-source.png`, כך שחלון ה-`Electron`, קובץ ה-`exe`, המתקין וה-uninstaller משתמשים באותו נכס רשמי +- `GitHub Releases` הוא provider נתמך ישירות על ידי `electron-builder`, ולכן התהליך קצר ויציב יותר מפתרון custom +- הפלט כולל `latest.yml`, כך שאם בהמשך תתווסף שכבת `electron-updater`, בסיס הפרסום כבר קיים +- ה-workflow מפריד בין `preview artifacts` לבין release אמיתי, כדי לא לפרסם כל build ללקוחות +- תוצרי ה-desktop נכתבים ל-`frontend/desktop-dist` ולא ל-`frontend/dist`, כדי למנוע רקורסיה מול משאבי ה-renderer שנארזים יחד עם האפליקציה + +## מגבלות נוכחיות + +- המתקין עדיין לא self-contained מבחינת backend: + - ה-`Electron shell` אורז את קוד ה-`FastAPI` כ-source resources + - בזמן ריצה האפליקציה עדיין מצפה ל-`Python` זמין במכונה של המשתמש +- ה-release כרגע לא מבצע `code signing` + - ב-`Windows`, אפליקציה לא חתומה עלולה לקבל אזהרות `SmartScreen` + - ה-workflow מבטל `CSC_IDENTITY_AUTO_DISCOVERY` כדי למנוע כשלי build בסביבה לא חתומה + +## תחזוקת אייקון האפליקציה + +- מקור האמת של האייקון הוא `album_deduplicator/frontend/build/icons/app-icon-source.png` +- `npm run sync:icons` יוצר/מעדכן ממנו את: + - `frontend/build/icons/app-icon.ico` + - `frontend/public/app-icon.png` + - `frontend/electron/assets/icons/app-icon.png` + - `frontend/electron/assets/icons/app-icon.ico` +- `npm run build`, `npm run electron`, ו-`npm run dev:electron` מריצים את הסנכרון הזה אוטומטית, כך שכל build או הרצה של מעטפת ה-Desktop משתמשים בגרסה המעודכנת של האייקון + +## שלב מומלץ הבא + +אם רוצים חוויית התקנה טובה יותר למשתמשי `Windows`, שני השיפורים הטבעיים הבאים הם: + +1. לארוז את ה-backend כ-executable, כדי להסיר את הדרישה ל-`Python` מותקן מראש +2. להוסיף `code signing`, רצוי דרך `Azure Trusted Signing` או תהליך signing ארגוני אחר שנתמך על ידי `electron-builder` diff --git a/album_deduplicator/docs/backlog/PLAN.md b/album_deduplicator/docs/backlog/PLAN.md new file mode 100644 index 0000000..f1da209 --- /dev/null +++ b/album_deduplicator/docs/backlog/PLAN.md @@ -0,0 +1,90 @@ +# תוכנית: ממשק מודרני עם React + API, מחיקה בטוחה, ו-AI כברירת מחדל + +## Summary +- הוחלף ממשק ה-`Streamlit` בממשק `React` מודרני, בעברית, desktop-first אך רספונסיבי, עם זרימה קצרה וברורה: בחירת תיקיות, סריקה, סקירת תוצאות, אישור מחיקה. +- להוסיף שכבת `FastAPI` מסודרת מעל הלוגיקה הקיימת, כך שהפרונטנד לא יריץ ישירות את מנוע הסריקה/ההשוואה, וה-CLI וה-API ישתמשו באותם services. +- להפוך את ה-ML המקומי לברירת מחדל אמיתית: תמיד לחשב `algorithmic_score` וגם `ml_score`, ולשלב אותם קבוע ל-`base_score` במקום שה-ML יחליף את הניקוד המתמטי. +- להשאיר `Gemini` כאימות גבולי בלבד: לא חובה, לא חוסם שימוש, ולא קובע לבדו מחיקה אוטומטית. +- לשנות את חוויית המחיקה כך שהפעולה הראשית תהיה “שמור את האלבום המומלץ”, עם מסך “Safe to delete” נפרד ומוגן, ולא רשימת selectboxes על כל זוג. + +## Implementation Changes +- לפרק את האורקסטרציה הקיימת מתוך `main.py` וממשק ה-`Streamlit` הישן לשכבת services: + - `AnalysisOrchestrator` לניהול סריקה, השוואה, איכות, scoring, cache ו-progress. + - `ScoringService` לחישוב `algorithmic_score`, `ml_score`, `base_score`, `gemini_score`, `final_score`. + - `RecommendationService` ליצירת clusters, בחירת keeper מומלץ, הסבר החלטה, וסיווג `safe` מול `review`. + - `DeletionService` ל-preview ולביצוע `send2trash` בלבד. +- לקבע מדיניות ניקוד: + - `base_score = 0.35 * algorithmic_score + 0.65 * ml_score`. + - אם `Gemini` זמין ורץ על זוג גבולי, `final_score = 0.85 * base_score + 0.15 * gemini_score`. + - אם מודל ML לא זמין, לחפש אוטומטית קודם ב-`album_deduplicator/data`, ואם לא נמצא אז ב-`similarity_model/models`; רק אם גם שם חסר, לרדת ל-`algorithmic_score` ולסמן מצב degraded ב-API וב-UI. + - `Gemini` ירוץ רק על זוגות עם `60 < base_score <= 90`, שאינם `identical_by_hash`, ואינו נדרש כדי להציג תוצאות. +- לקבע מדיניות מחיקה בטוחה: + - מחיקה קבוצתית אוטומטית תוצע רק לזוגות/קבוצות עם אותו מספר קבצי מוזיקה, וללא תמיכה ב-v1 במהדורות עם מספר שירים שונה. + - `safe_delete` יתקבל רק אם `is_identical_by_hash == true` או `final_score >= 97`, ובנוסף יש keeper יחיד ברור לפי `preferred_root` ואז `quality_score`, בלי התנגשות החלטות בתוך cluster. + - זוגות בטווח `85-96.99` ייכנסו ל-`review` בלבד. + - `Gemini` לא יוכל לבדו לקדם פריט ל-`safe_delete`; הוא יכול רק לחזק/להחליש מקרה גבולי לסקירה. + - כל מחיקה תישאר `Recycle Bin` בלבד; אין hard delete. +- לבנות UX חדש סביב clusters ולא סביב שורות בודדות: + - מסך פתיחה מינימלי עם תיקיות קלט, תיקיית root מועדפת, וכפתור ראשי אחד לניתוח. + - אפשרויות מתקדמות יוסתרו ב-drawer; ML לא יוצג כבחירה רגילה אלא כמצב ברירת מחדל פעיל. + - מסך תוצאות יחולק ל-3 tabs: `בטוח למחיקה`, `דורש סקירה`, `כל התוצאות`. + - כל cluster יוצג כ-card עם keeper מומלץ, סיבת ההמלצה, confidence, איכות, ופעולות מהירות: `שמור מומלץ`, `שנה keeper`, `דלג`. + - חלונית side panel תציג פירוט השוואה, tracklist מאוחד, score breakdown, Gemini reason אם קיים, והסבר למה הפריט סווג כ-safe או review. + - מסך אישור מחיקה יציג preview מרוכז: כמה תיקיות יועברו לסל, אילו יישמרו, ואישור מפורש אחד לפני ביצוע. +- לשפר את שכבת התקשורת פרונט/באק: + - ניהול ניתוחים כ-`analysis sessions` עם מזהה יציב. + - progress דרך `SSE` ולא polling אגרסיבי. + - DTOs יציבים עם IDs לקבוצות/זוגות/תיקיות, במקום הסתמכות על tuples של נתיבים בתוך ה-UI. + - שמירת user decisions בשרת כדי לאפשר רענון דף בלי לאבד בחירות. + +## Public APIs / Interfaces +- `POST /api/analysis-sessions` + - קלט: `folders[]`, `preferred_root`, `force_rescan`, `clear_cache`, `bitrate_mode`, `gemini_enabled`. + - פלט: `session_id`, `status=queued`. +- `GET /api/analysis-sessions/{session_id}` + - פלט: `status`, `progress`, `mode_summary`, `degraded_flags`, `counts`. +- `GET /api/analysis-sessions/{session_id}/events` + - `SSE` עבור progress, שלבים, warnings, וסיום. +- `GET /api/analysis-sessions/{session_id}/clusters?bucket=safe|review|all` + - פלט: clusters עם `cluster_id`, `recommended_keeper_id`, `confidence_bucket`, `reason_codes`, summaries של albums ו-pairs. +- `POST /api/analysis-sessions/{session_id}/decisions` + - קלט: רשימת החלטות `cluster_id -> keeper_id | skip`. + - פלט: delete preview מעודכן. +- `GET /api/analysis-sessions/{session_id}/delete-preview` + - פלט: רשימת תיקיות למחיקה, keeper לכל אחת, deduplication של כפילויות בין זוגות, וספירות כוללות. +- `POST /api/analysis-sessions/{session_id}/delete-executions` + - קלט: רשימת `folder_ids` מאושרים למחיקה. + - פלט: `moved_count`, `failed_count`, `results[]`. +- DTOs חדשים: + - `AnalysisSession`, `FolderSummary`, `PairScoreBreakdown`, `ClusterSummary`, `RecommendationReason`, `DeletePreviewItem`, `DeleteExecutionResult`. + +## Test Plan +- בדיקות unit ל-`ScoringService`: + - שילוב קבוע של algorithmic+ML. + - fallback כאשר המודל חסר. + - שילוב Gemini רק בטווח הגבולי ורק כמשקל משני. +- בדיקות unit ל-`RecommendationService`: + - בחירת keeper לפי `preferred_root` ואז `quality_score`. + - סיווג נכון ל-`safe` מול `review`. + - מניעת bulk delete כשאין keeper יחיד ברור. +- בדיקות API: + - יצירת session, דיווח progress, שליפת clusters, שמירת decisions, delete preview, וביצוע מחיקה. + - החזרת `degraded_flags` כש-ML/Gemini חסרים. +- בדיקות frontend: + - הצגת מסך פתיחה מינימלי והסתרת advanced settings. + - ברירת מחדל ל-tab `בטוח למחיקה`. + - card של cluster עם keeper מומלץ ו-action מהיר. + - delete confirmation מציג preview נכון ולא מאפשר ביצוע בלי אישור. +- בדיקות end-to-end עם fixture directories: + - exact duplicates נכנסים ל-`safe`. + - זוג גבולי עם אותו מבנה נכנס ל-`review`. + - זוג עם מספר קבצים שונה לא מוצע למחיקה ולא מופיע ב-v1. + - מחיקה שולחת ל-Recycle Bin בלבד ומחזירה תוצאת ביצוע תקינה. + +## Assumptions and Defaults +- היישום הוא local single-user על Windows, עם UI עברי כברירת מחדל. +- ה-UI הראשי הוא `React + API`, ללא fallback של `Streamlit`. +- ה-CLI יישאר, אך ישתמש באותה שכבת services כדי למנוע לוגיקה כפולה. +- אין תמיכה ב-v1 בזיהוי והשוואה של אלבומים עם מספר קבצים שונה, לפי ההעדפה שנקבעה. +- ברירת המחדל למחיקה אוטומטית היא שמרנית מאוד: `safe_delete` רק מעל `97` או `identical_by_hash`. +- `Gemini` הוא enhancement בלבד: אם אין API key או dependencies, המערכת נשארת שמישה לחלוטין עם algorithmic+ML. diff --git a/album_deduplicator/experiments/api_gemini_demo.ipynb b/album_deduplicator/experiments/api_gemini_demo.ipynb new file mode 100644 index 0000000..9bb9190 --- /dev/null +++ b/album_deduplicator/experiments/api_gemini_demo.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "בתמונה רואים דמות דמוית ילד קטן, עם עור כחול, שיער לבן וקרניים שחורות קטנות, לובשת בגדים בסגנון אביר ורודפת אחרי יצור גדול דמוי דינוזאור סגול עם קוצים ושיניים. הרקע הוא נוף מדברי עם הרי חול, מבנים הרוסים וקווי טלפון." + ] + } + ], + "source": [ + "import base64\n", + "import os\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "\n", + "def generate():\n", + " client = genai.Client(\n", + " api_key=os.environ.get(\"GEMINI_API_KEY\"),\n", + " )\n", + "\n", + " files = [\n", + " # Please ensure that the file is available in local system working direrctory or change the file path.\n", + " client.files.upload(file=\"AlbumArtSmall.jpg\"),\n", + " ]\n", + " model = \"gemini-2.0-flash-lite\"\n", + " contents = [\n", + " types.Content(\n", + " role=\"user\",\n", + " parts=[\n", + " types.Part.from_text(text=\"\"\"{json content}\"\"\"),\n", + " types.Part.from_uri(\n", + " file_uri=files[0].uri,\n", + " mime_type=files[0].mime_type,\n", + " ),\n", + " types.Part.from_text(text=\"\"\"INSERT_INPUT_HERE\"\"\"),\n", + " ],\n", + " ),\n", + " ]\n", + " generate_content_config = types.GenerateContentConfig(\n", + " response_mime_type=\"application/json\",\n", + " response_schema=genai.types.Schema(\n", + " type = genai.types.Type.OBJECT,\n", + " required = [\"is_duplicate\", \"confidence\", \"reason\"],\n", + " properties = {\n", + " \"is_duplicate\": genai.types.Schema(\n", + " type = genai.types.Type.BOOLEAN,\n", + " description = \"Boolean indicating if the item is a duplicate\",\n", + " ),\n", + " \"confidence\": genai.types.Schema(\n", + " type = genai.types.Type.INTEGER,\n", + " description = \"Confidence score between 0 and 100\",\n", + " ),\n", + " \"reason\": genai.types.Schema(\n", + " type = genai.types.Type.STRING,\n", + " description = \"Short explanation in Hebrew\",\n", + " ),\n", + " },\n", + " ),\n", + " system_instruction=[\n", + " types.Part.from_text(text=\"\"\"insturct content\"\"\"),\n", + " ],\n", + " )\n", + "\n", + " for chunk in client.models.generate_content_stream(\n", + " model=model,\n", + " contents=contents,\n", + " config=generate_content_config,\n", + " ):\n", + " print(chunk.text, end=\"\")\n", + "\n", + "if __name__ == \"__main__\":\n", + " generate()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/album_deduplicator/frontend/build/icons/app-icon-source.png b/album_deduplicator/frontend/build/icons/app-icon-source.png new file mode 100644 index 0000000..07bc693 Binary files /dev/null and b/album_deduplicator/frontend/build/icons/app-icon-source.png differ diff --git a/album_deduplicator/frontend/electron-builder.config.cjs b/album_deduplicator/frontend/electron-builder.config.cjs new file mode 100644 index 0000000..888635a --- /dev/null +++ b/album_deduplicator/frontend/electron-builder.config.cjs @@ -0,0 +1,57 @@ +const { resolveAppVersion } = require("./electron/app-version.cjs"); + +const appVersion = resolveAppVersion(); + +module.exports = { + appId: "com.musicautomatic.albumdeduplicator", + productName: "Music Automatic", + directories: { + buildResources: "build", + output: "desktop-dist", + }, + files: [ + "dist/**/*", + "electron/**/*", + "package.json", + ], + extraResources: [ + { + from: "../api", + to: "backend-source/api", + }, + { + from: "../music_dup_lib", + to: "backend-source/music_dup_lib", + }, + { + from: "../data/lgbm_regressor_model.joblib", + to: "backend-source/data/lgbm_regressor_model.joblib", + }, + { + from: "../frontend/dist", + to: "backend-source/frontend/dist", + }, + ], + extraMetadata: { + version: appVersion, + }, + win: { + target: "nsis", + icon: "build/icons/app-icon.ico", + }, + nsis: { + installerIcon: "build/icons/app-icon.ico", + uninstallerIcon: "build/icons/app-icon.ico", + installerHeaderIcon: "build/icons/app-icon.ico", + }, + publish: [ + { + provider: "github", + owner: "NHLOCAL", + repo: "Music-Automatic", + tagNamePrefix: "album-deduplicator-v", + releaseType: "release", + }, + ], + artifactName: "${productName}-${version}-setup-${arch}.${ext}", +}; diff --git a/album_deduplicator/frontend/electron-builder.config.test.js b/album_deduplicator/frontend/electron-builder.config.test.js new file mode 100644 index 0000000..c6b9fa5 --- /dev/null +++ b/album_deduplicator/frontend/electron-builder.config.test.js @@ -0,0 +1,14 @@ +import { createRequire } from "node:module"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const builderConfig = require("./electron-builder.config.cjs"); + +describe("electron builder config", () => { + it("packages the ML model where the backend config resolves it", () => { + expect(builderConfig.extraResources).toContainEqual({ + from: "../data/lgbm_regressor_model.joblib", + to: "backend-source/data/lgbm_regressor_model.joblib", + }); + }); +}); diff --git a/album_deduplicator/frontend/electron/app-version.cjs b/album_deduplicator/frontend/electron/app-version.cjs new file mode 100644 index 0000000..266afe6 --- /dev/null +++ b/album_deduplicator/frontend/electron/app-version.cjs @@ -0,0 +1,109 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); + +const TAG_PREFIX = "album-deduplicator-v"; +const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; + +function getPackageJsonPath() { + return path.join(__dirname, "..", "package.json"); +} + +function normalizeVersion(version, source) { + const cleanVersion = String(version ?? "").trim(); + if (!SEMVER_PATTERN.test(cleanVersion)) { + throw new Error(`Invalid Album Deduplicator version from ${source}: "${cleanVersion}"`); + } + return cleanVersion; +} + +function parseVersionFromTag(tagName) { + const cleanTagName = String(tagName ?? "").trim(); + if (!cleanTagName) return null; + if (!cleanTagName.startsWith(TAG_PREFIX)) return null; + return normalizeVersion(cleanTagName.slice(TAG_PREFIX.length), `git tag ${cleanTagName}`); +} + +function readPackageVersion(packageJsonPath = getPackageJsonPath()) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + return normalizeVersion(packageJson.version, packageJsonPath); +} + +function shouldIgnoreGitError(error) { + if (!error) return false; + if (error.code === "ENOENT") return true; + return error.status === 128; +} + +function getGitTagVersion({ + cwd = path.join(__dirname, ".."), + execFileSyncImpl = execFileSync, +} = {}) { + try { + const output = execFileSyncImpl("git", ["tag", "--points-at", "HEAD"], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + const versions = output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map(parseVersionFromTag) + .filter(Boolean); + + const uniqueVersions = Array.from(new Set(versions)); + if (uniqueVersions.length > 1) { + throw new Error(`Multiple Album Deduplicator release tags point at HEAD: ${uniqueVersions.join(", ")}`); + } + + return uniqueVersions[0] ?? null; + } catch (error) { + if (shouldIgnoreGitError(error)) { + return null; + } + throw error; + } +} + +function resolveAppVersion({ + env = process.env, + packageJsonPath = getPackageJsonPath(), + cwd = path.dirname(packageJsonPath), + packageVersion, + gitTagVersion, + execFileSyncImpl = execFileSync, +} = {}) { + if (env.ALBUM_DEDUP_VERSION) { + return normalizeVersion(env.ALBUM_DEDUP_VERSION, "ALBUM_DEDUP_VERSION"); + } + + const githubTagVersion = parseVersionFromTag(env.GITHUB_REF_NAME); + if (githubTagVersion) { + return githubTagVersion; + } + + const resolvedGitTagVersion = gitTagVersion !== undefined + ? gitTagVersion + : getGitTagVersion({ cwd, execFileSyncImpl }); + if (resolvedGitTagVersion) { + return normalizeVersion(resolvedGitTagVersion, "git tag on HEAD"); + } + + return packageVersion !== undefined + ? normalizeVersion(packageVersion, "package version override") + : readPackageVersion(packageJsonPath); +} + +if (require.main === module) { + process.stdout.write(`${resolveAppVersion()}\n`); +} + +module.exports = { + TAG_PREFIX, + getGitTagVersion, + normalizeVersion, + parseVersionFromTag, + readPackageVersion, + resolveAppVersion, +}; diff --git a/album_deduplicator/frontend/electron/app-version.test.js b/album_deduplicator/frontend/electron/app-version.test.js new file mode 100644 index 0000000..90f0609 --- /dev/null +++ b/album_deduplicator/frontend/electron/app-version.test.js @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; + +import appVersionModule from "./app-version.cjs"; + +const { getGitTagVersion, parseVersionFromTag, resolveAppVersion } = appVersionModule; + +describe("app version resolver", () => { + it("extracts a semantic version from the release tag", () => { + expect(parseVersionFromTag("album-deduplicator-v1.2.3")).toBe("1.2.3"); + expect(parseVersionFromTag("album-deduplicator-v1.2.3-beta.1")).toBe("1.2.3-beta.1"); + expect(parseVersionFromTag("other-tag-v1.2.3")).toBeNull(); + }); + + it("fails fast for an invalid release tag format", () => { + expect(() => parseVersionFromTag("album-deduplicator-vnext")).toThrow(/Invalid Album Deduplicator version/); + }); + + it("prefers an explicit environment override", () => { + expect(resolveAppVersion({ + env: { + ALBUM_DEDUP_VERSION: "3.4.5", + GITHUB_REF_NAME: "album-deduplicator-v1.2.3", + }, + gitTagVersion: "2.0.0", + packageVersion: "0.1.0", + })).toBe("3.4.5"); + }); + + it("uses the GitHub tag name when present", () => { + expect(resolveAppVersion({ + env: { GITHUB_REF_NAME: "album-deduplicator-v1.2.3" }, + gitTagVersion: null, + packageVersion: "0.1.0", + })).toBe("1.2.3"); + }); + + it("uses an exact local git tag when no CI tag is present", () => { + expect(resolveAppVersion({ + env: {}, + gitTagVersion: "2.5.0", + packageVersion: "0.1.0", + })).toBe("2.5.0"); + }); + + it("falls back to package.json version outside a tagged release", () => { + expect(resolveAppVersion({ + env: {}, + gitTagVersion: null, + packageVersion: "0.1.0", + })).toBe("0.1.0"); + }); + + it("ignores missing git metadata and returns null", () => { + expect(getGitTagVersion({ + execFileSyncImpl: () => { + const error = new Error("git not found"); + error.code = "ENOENT"; + throw error; + }, + })).toBeNull(); + }); +}); diff --git a/album_deduplicator/frontend/electron/assets/icons/app-icon.ico b/album_deduplicator/frontend/electron/assets/icons/app-icon.ico new file mode 100644 index 0000000..71c6437 Binary files /dev/null and b/album_deduplicator/frontend/electron/assets/icons/app-icon.ico differ diff --git a/album_deduplicator/frontend/electron/assets/icons/app-icon.png b/album_deduplicator/frontend/electron/assets/icons/app-icon.png new file mode 100644 index 0000000..07bc693 Binary files /dev/null and b/album_deduplicator/frontend/electron/assets/icons/app-icon.png differ diff --git a/album_deduplicator/frontend/electron/feedback-export-filename.cjs b/album_deduplicator/frontend/electron/feedback-export-filename.cjs new file mode 100644 index 0000000..0ff470c --- /dev/null +++ b/album_deduplicator/frontend/electron/feedback-export-filename.cjs @@ -0,0 +1,38 @@ +const crypto = require("node:crypto"); +const os = require("node:os"); + +function sanitizeFilenamePart(value, fallback) { + const cleaned = String(value || "") + .trim() + .replace(/[<>:"/\\|?*\x00-\x1f]+/g, "-") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 40); + return cleaned || fallback; +} + +function formatTimestamp(date = new Date()) { + return date.toISOString().replace( + /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*$/, + "$1$2$3-$4$5$6", + ); +} + +function buildFeedbackExportFilename({ + date = new Date(), + username = os.userInfo().username, + hostname = os.hostname(), + randomId = crypto.randomBytes(3).toString("hex"), +} = {}) { + const safeUser = sanitizeFilenamePart(username, "user"); + const safeHost = sanitizeFilenamePart(hostname, "machine"); + const safeId = sanitizeFilenamePart(randomId, "export"); + return `ma-feedback_${formatTimestamp(date)}_${safeUser}-${safeHost}_${safeId}.jsonl`; +} + +module.exports = { + buildFeedbackExportFilename, + formatTimestamp, + sanitizeFilenamePart, +}; diff --git a/album_deduplicator/frontend/electron/feedback-export-filename.test.js b/album_deduplicator/frontend/electron/feedback-export-filename.test.js new file mode 100644 index 0000000..decec6e --- /dev/null +++ b/album_deduplicator/frontend/electron/feedback-export-filename.test.js @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import filenameModule from "./feedback-export-filename.cjs"; + +const { buildFeedbackExportFilename, sanitizeFilenamePart } = filenameModule; + +describe("feedback export filename", () => { + it("builds a descriptive JSONL filename that distinguishes runs and machines", () => { + const filename = buildFeedbackExportFilename({ + date: new Date("2026-05-15T12:34:56.000Z"), + username: "me user", + hostname: "studio-pc", + randomId: "a1b2c3", + }); + + expect(filename).toBe("ma-feedback_20260515-123456_me-user-studio-pc_a1b2c3.jsonl"); + }); + + it("sanitizes characters that are invalid in Windows filenames", () => { + expect(sanitizeFilenamePart('ac:d/e\\f|g?h*i', "fallback")).toBe("a-b-c-d-e-f-g-h-i"); + }); +}); diff --git a/album_deduplicator/frontend/electron/main.cjs b/album_deduplicator/frontend/electron/main.cjs new file mode 100644 index 0000000..3302aa8 --- /dev/null +++ b/album_deduplicator/frontend/electron/main.cjs @@ -0,0 +1,322 @@ +const { app, BrowserWindow, dialog, ipcMain, shell } = require("electron"); +const { spawn } = require("node:child_process"); +const fs = require("node:fs/promises"); +const net = require("node:net"); +const path = require("node:path"); +const { resolveAppVersion } = require("./app-version.cjs"); +const { buildFeedbackExportFilename } = require("./feedback-export-filename.cjs"); + +const ELECTRON_DEV_URL = process.env.ELECTRON_RENDERER_URL || "http://127.0.0.1:5173"; +const BACKEND_HOST = "127.0.0.1"; +const BACKEND_START_TIMEOUT_MS = 30_000; +const DESKTOP_APP_VERSION = resolveAppVersion(); + +let backendProcess = null; +let backendBaseUrl = null; +let mainWindow = null; + +function isDev() { + return !app.isPackaged; +} + +function logBackendOutput(stream, label) { + stream.on("data", (chunk) => { + const text = chunk.toString().trim(); + if (text) { + console.log(`[backend:${label}] ${text}`); + } + }); +} + +function getBackendSourceRoot() { + if (app.isPackaged) { + return path.join(process.resourcesPath, "backend-source"); + } + return path.resolve(__dirname, "..", ".."); +} + +function getPackagedBackendExecutable() { + const extension = process.platform === "win32" ? ".exe" : ""; + return path.join(process.resourcesPath, "backend", `album-deduplicator-api${extension}`); +} + +function getWindowIconPath() { + const iconFileName = process.platform === "win32" ? "app-icon.ico" : "app-icon.png"; + return path.join(__dirname, "assets", "icons", iconFileName); +} + +function getSpawnCandidates(port) { + const sourceRoot = getBackendSourceRoot(); + const sharedArgs = [ + "-m", + "uvicorn", + "api.app:app", + "--host", + BACKEND_HOST, + "--port", + String(port), + ]; + + const candidates = []; + const packagedExecutable = getPackagedBackendExecutable(); + candidates.push({ + command: packagedExecutable, + args: [], + cwd: path.dirname(packagedExecutable), + mode: "packaged-executable", + }); + + const rawCommands = process.platform === "win32" + ? [process.env.ALBUM_DEDUP_PYTHON, process.env.PYTHON, "py", "python"] + : [process.env.ALBUM_DEDUP_PYTHON, process.env.PYTHON, "python3", "python"]; + + const seen = new Set(); + rawCommands.filter(Boolean).forEach((command) => { + const args = command === "py" ? ["-3", ...sharedArgs] : sharedArgs; + const key = `${command}::${args.join(" ")}`; + if (!seen.has(key)) { + seen.add(key); + candidates.push({ + command, + args, + cwd: sourceRoot, + mode: "python-source", + }); + } + }); + + return candidates; +} + +function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on("error", reject); + server.listen(0, BACKEND_HOST, () => { + const address = server.address(); + const port = typeof address === "object" && address ? address.port : null; + server.close((closeError) => { + if (closeError) { + reject(closeError); + return; + } + resolve(port); + }); + }); + }); +} + +async function waitForBackend(url, timeoutMs) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + try { + const response = await fetch(`${url}/api/health`); + if (response.ok) { + return; + } + } catch (error) { + // Ignore connection errors until timeout. + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + throw new Error(`Backend did not become ready within ${timeoutMs}ms.`); +} + +function stopBackend() { + if (!backendProcess || backendProcess.killed) { + return; + } + + const pid = backendProcess.pid; + if (process.platform === "win32") { + spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { + windowsHide: true, + stdio: "ignore", + }); + } else { + backendProcess.kill("SIGTERM"); + } + backendProcess = null; +} + +async function startBackend() { + const port = await getFreePort(); + const url = `http://${BACKEND_HOST}:${port}`; + const candidates = getSpawnCandidates(port); + let lastError = null; + + for (const candidate of candidates) { + try { + const child = spawn(candidate.command, candidate.args, { + cwd: candidate.cwd, + env: { + ...process.env, + ALBUM_DEDUP_VERSION: DESKTOP_APP_VERSION, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + }, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + + backendProcess = child; + logBackendOutput(child.stdout, "stdout"); + logBackendOutput(child.stderr, "stderr"); + + const exited = new Promise((_, reject) => { + child.once("exit", (code, signal) => { + reject(new Error(`Backend exited early (${candidate.mode}) with code=${code} signal=${signal}`)); + }); + child.once("error", reject); + }); + + await Promise.race([waitForBackend(url, BACKEND_START_TIMEOUT_MS), exited]); + backendBaseUrl = url; + return url; + } catch (error) { + lastError = error; + stopBackend(); + } + } + + throw lastError || new Error("Unable to launch backend."); +} + +function setupIpcHandlers() { + ipcMain.handle("desktop:get-runtime", () => ({ + isElectron: true, + backendBaseUrl, + version: DESKTOP_APP_VERSION, + platform: process.platform, + })); + + ipcMain.handle("desktop:pick-scan-folders", async (_event, options = {}) => { + const defaultPath = typeof options.defaultPath === "string" && options.defaultPath.trim() + ? options.defaultPath.trim() + : undefined; + const allowMultiple = options.allowMultiple !== false; + const title = allowMultiple ? "בחר תיקיות לסריקה" : "בחר תיקייה לסריקה"; + const buttonLabel = allowMultiple ? "הוסף תיקיות" : "בחר תיקייה"; + const properties = allowMultiple ? ["openDirectory", "multiSelections"] : ["openDirectory"]; + + const result = await dialog.showOpenDialog(mainWindow, { + title, + buttonLabel, + defaultPath, + properties, + }); + return result.canceled ? [] : result.filePaths; + }); + + ipcMain.handle("desktop:pick-preferred-root", async () => { + const result = await dialog.showOpenDialog(mainWindow, { + title: "בחר תיקייה מועדפת לשמירה", + properties: ["openDirectory"], + }); + return result.canceled ? null : (result.filePaths[0] || null); + }); + + ipcMain.handle("desktop:open-path", async (_event, targetPath) => { + const error = await shell.openPath(targetPath); + return { ok: !error, error: error || null }; + }); + + ipcMain.handle("desktop:reveal-path", async (_event, targetPath) => { + shell.showItemInFolder(targetPath); + return { ok: true }; + }); + + ipcMain.handle("desktop:export-feedback", async (_event, exportUrl) => { + const requestedUrl = new URL(exportUrl); + const backendUrl = new URL(backendBaseUrl); + if (requestedUrl.origin !== backendUrl.origin || requestedUrl.pathname !== "/api/ml-feedback/export") { + throw new Error("Invalid feedback export URL."); + } + + const result = await dialog.showSaveDialog(mainWindow, { + title: "יצוא נתוני אימון", + defaultPath: buildFeedbackExportFilename(), + filters: [ + { name: "JSON Lines", extensions: ["jsonl"] }, + { name: "All Files", extensions: ["*"] }, + ], + }); + if (result.canceled || !result.filePath) { + return { ok: false, canceled: true }; + } + + const response = await fetch(requestedUrl.toString()); + if (!response.ok) { + throw new Error(`Failed to export feedback data: ${response.status}`); + } + const content = Buffer.from(await response.arrayBuffer()); + await fs.writeFile(result.filePath, content); + shell.showItemInFolder(result.filePath); + return { ok: true, canceled: false, filePath: result.filePath }; + }); +} + +async function createMainWindow() { + mainWindow = new BrowserWindow({ + width: 1600, + height: 980, + minWidth: 1280, + minHeight: 820, + show: false, + backgroundColor: "#f4f1ea", + icon: getWindowIconPath(), + title: "Music Automatic", + webPreferences: { + preload: path.join(__dirname, "preload.cjs"), + contextIsolation: true, + nodeIntegration: false, + additionalArguments: [ + `--backend-base-url=${backendBaseUrl}`, + `--app-version=${DESKTOP_APP_VERSION}`, + ], + }, + }); + + mainWindow.once("ready-to-show", () => { + mainWindow.show(); + }); + + mainWindow.on("closed", () => { + mainWindow = null; + }); + + if (isDev()) { + await mainWindow.loadURL(ELECTRON_DEV_URL); + mainWindow.webContents.openDevTools({ mode: "detach" }); + return; + } + + await mainWindow.loadURL(`${backendBaseUrl}/`); +} + +app.on("window-all-closed", () => { + stopBackend(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopBackend(); +}); + +app.whenReady().then(async () => { + try { + await startBackend(); + setupIpcHandlers(); + await createMainWindow(); + } catch (error) { + console.error(error); + dialog.showErrorBox( + "Album Deduplicator failed to start", + `${error.message}\n\nודא ש-Python זמין במערכת ושה-dependencies של ה-backend מותקנות.`, + ); + app.quit(); + } +}); diff --git a/album_deduplicator/frontend/electron/preload.cjs b/album_deduplicator/frontend/electron/preload.cjs new file mode 100644 index 0000000..1f79233 --- /dev/null +++ b/album_deduplicator/frontend/electron/preload.cjs @@ -0,0 +1,23 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +function readArgument(prefix) { + const entry = process.argv.find((value) => value.startsWith(prefix)); + return entry ? entry.slice(prefix.length) : ""; +} + +const runtime = Object.freeze({ + isElectron: true, + backendBaseUrl: readArgument("--backend-base-url="), + version: readArgument("--app-version="), + platform: process.platform, +}); + +contextBridge.exposeInMainWorld("albumDeduplicator", { + runtime, + getRuntimeInfo: () => ipcRenderer.invoke("desktop:get-runtime"), + selectScanFolders: (options) => ipcRenderer.invoke("desktop:pick-scan-folders", options), + selectPreferredRoot: () => ipcRenderer.invoke("desktop:pick-preferred-root"), + openPath: (targetPath) => ipcRenderer.invoke("desktop:open-path", targetPath), + revealPath: (targetPath) => ipcRenderer.invoke("desktop:reveal-path", targetPath), + exportFeedback: (exportUrl) => ipcRenderer.invoke("desktop:export-feedback", exportUrl), +}); diff --git a/album_deduplicator/frontend/index.html b/album_deduplicator/frontend/index.html new file mode 100644 index 0000000..b2d178f --- /dev/null +++ b/album_deduplicator/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + Music Automatic + + + + + + + +
+ + diff --git a/album_deduplicator/frontend/package-lock.json b/album_deduplicator/frontend/package-lock.json new file mode 100644 index 0000000..744a40f --- /dev/null +++ b/album_deduplicator/frontend/package-lock.json @@ -0,0 +1,9023 @@ +{ + "name": "album-deduplicator-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "album-deduplicator-frontend", + "version": "0.1.0", + "dependencies": { + "@ant-design/happy-work-theme": "^2.0.0", + "@ant-design/icons": "^6.1.0", + "@fontsource/rubik": "^5.2.8", + "antd": "^6.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@vitejs/plugin-react": "^4.4.1", + "concurrently": "^9.2.1", + "cross-env": "^10.0.0", + "electron": "^37.2.0", + "electron-builder": "^26.0.12", + "jsdom": "^26.0.0", + "vite": "^6.2.0", + "vitest": "^3.0.8", + "wait-on": "^8.0.5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/happy-work-theme": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/happy-work-theme/-/happy-work-theme-2.0.0.tgz", + "integrity": "sha512-3pvwSOGl9YKkhusTFVsSAouoQPJNsf95I2ooIiUmG/tOkayFUbFqPqMHkMc5Q0XdpfKW+sCr6bCtLdsDnWxaaA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.0.1", + "@ant-design/fast-color": "^3.0.0", + "@babel/runtime": "^7.18.3", + "@rc-component/motion": "^1.1.6", + "@rc-component/util": "^1.5.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "antd": ">=6.1.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "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.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "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/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.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.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/rubik": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/rubik/-/rubik-5.2.8.tgz", + "integrity": "sha512-PIc8QR7FqWPcYhbdRiGff56vQlKqg/ytES1YqecSq1GkgxiH4TBshrFvDEOZ9JonUF9m1qQ+qXxJj7wD5zgXEw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.14.0.tgz", + "integrity": "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.2.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.4.tgz", + "integrity": "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.6.2.tgz", + "integrity": "sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.6.2", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.6.0.tgz", + "integrity": "sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", + "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", + "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/textarea": "~1.1.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", + "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.1.tgz", + "integrity": "sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", + "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz", + "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.0.tgz", + "integrity": "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.1.tgz", + "integrity": "sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.14.tgz", + "integrity": "sha512-T1IWeLlSas7Z/igZtPtJ/bweCxMMkXIGKQBtnigK+I/n1AVNjCs+ZdL3Fj42mq3uqm4sd1uzeQLZkdCqR26ADw==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz", + "integrity": "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", + "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/textarea": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", + "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz", + "integrity": "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.2.3.tgz", + "integrity": "sha512-mG8hF2ogQcKaEpfyxzPvMWqqkptofd7Sf+YiXOpPzuXLTLwNKfLDJtysc1/oybopbnzxNqWh2Vgwi+GYwNIb7w==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.8.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.8.0.tgz", + "integrity": "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.2.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", + "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", + "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", + "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.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.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "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/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "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/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.1.tgz", + "integrity": "sha512-8pRjvxitZFyrYAtgwml93Km7fCXjw9IeqlmzpIsusRsmO3eWFVrOMum6+0TsGCtR/WrXVnPwfsgrFg3ChzGCeA==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.0", + "@ant-design/cssinjs-utils": "^2.1.1", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.1.0", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.28.4", + "@rc-component/cascader": "~1.14.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.0", + "@rc-component/dialog": "~1.8.4", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.6.2", + "@rc-component/image": "~1.6.0", + "@rc-component/input": "~1.1.2", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.6.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.3.1", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~1.2.0", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.9.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.6.12", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.9.1", + "@rc-component/tabs": "~1.7.0", + "@rc-component/textarea": "~1.1.2", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.3.0", + "@rc-component/tree": "~1.2.3", + "@rc-component/tree-select": "~1.8.0", + "@rc-component/trigger": "^3.9.0", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/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/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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", + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "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/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "37.10.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz", + "integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "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/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "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/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", + "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "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/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "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", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "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.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "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/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "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/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "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" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/album_deduplicator/frontend/package.json b/album_deduplicator/frontend/package.json new file mode 100644 index 0000000..ebc6e70 --- /dev/null +++ b/album_deduplicator/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "album-deduplicator-frontend", + "private": true, + "version": "0.1.0", + "description": "Windows desktop shell for Album Deduplicator", + "author": "NHLOCAL", + "type": "module", + "main": "electron/main.cjs", + "scripts": { + "sync:icons": "python ./scripts/sync-icons.py", + "dev": "vite", + "dev:electron": "npm run sync:icons && concurrently -k \"vite --host 127.0.0.1 --port 5173\" \"wait-on tcp:127.0.0.1:5173 && cross-env ELECTRON_RENDERER_URL=http://127.0.0.1:5173 electron .\"", + "electron": "npm run sync:icons && electron .", + "build": "npm run sync:icons && vite build", + "build:desktop": "npm run build && electron-builder --config electron-builder.config.cjs --dir --publish never", + "dist:desktop": "npm run build && electron-builder --config electron-builder.config.cjs --publish never", + "dist:desktop:publish": "npm run build && electron-builder --config electron-builder.config.cjs --publish always", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "@ant-design/happy-work-theme": "^2.0.0", + "@ant-design/icons": "^6.1.0", + "@fontsource/rubik": "^5.2.8", + "antd": "^6.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@vitejs/plugin-react": "^4.4.1", + "concurrently": "^9.2.1", + "cross-env": "^10.0.0", + "electron": "^37.2.0", + "electron-builder": "^26.0.12", + "jsdom": "^26.0.0", + "vite": "^6.2.0", + "vitest": "^3.0.8", + "wait-on": "^8.0.5" + }, + "repository": { + "type": "git", + "url": "https://github.com/NHLOCAL/Music-Automatic.git" + } +} diff --git a/album_deduplicator/frontend/public/app-icon.png b/album_deduplicator/frontend/public/app-icon.png new file mode 100644 index 0000000..07bc693 Binary files /dev/null and b/album_deduplicator/frontend/public/app-icon.png differ diff --git a/album_deduplicator/frontend/scripts/sync-icons.py b/album_deduplicator/frontend/scripts/sync-icons.py new file mode 100644 index 0000000..c34be4a --- /dev/null +++ b/album_deduplicator/frontend/scripts/sync-icons.py @@ -0,0 +1,39 @@ +from pathlib import Path +import shutil + +from PIL import Image + + +ICON_SIZES = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (24, 24), (16, 16)] + + +def main() -> None: + script_dir = Path(__file__).resolve().parent + frontend_root = script_dir.parent + + build_icon_dir = frontend_root / "build" / "icons" + source_png = build_icon_dir / "app-icon-source.png" + build_ico = build_icon_dir / "app-icon.ico" + public_icon = frontend_root / "public" / "app-icon.png" + desktop_png = frontend_root / "electron" / "assets" / "icons" / "app-icon.png" + desktop_ico = frontend_root / "electron" / "assets" / "icons" / "app-icon.ico" + + if not source_png.is_file(): + raise FileNotFoundError(f"Missing source icon: {source_png}") + + for destination in (build_ico, public_icon, desktop_png, desktop_ico): + destination.parent.mkdir(parents=True, exist_ok=True) + + shutil.copyfile(source_png, public_icon) + shutil.copyfile(source_png, desktop_png) + + with Image.open(source_png) as source_image: + icon_image = source_image.convert("RGBA") + icon_image.save(build_ico, format="ICO", sizes=ICON_SIZES) + icon_image.save(desktop_ico, format="ICO", sizes=ICON_SIZES) + + print(f"Synced desktop icon assets from {source_png.name}") + + +if __name__ == "__main__": + main() diff --git a/album_deduplicator/frontend/src/App.jsx b/album_deduplicator/frontend/src/App.jsx new file mode 100644 index 0000000..685f927 --- /dev/null +++ b/album_deduplicator/frontend/src/App.jsx @@ -0,0 +1,394 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { App as AntApp, ConfigProvider, Layout } from "antd"; +import { ExclamationCircleOutlined } from "@ant-design/icons"; +import heIL from "antd/locale/he_IL"; +import { useDeduplicator } from "./hooks/useDeduplicator"; +import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; +import * as api from "./api"; +import { exportFeedbackFile, getRuntimeInfo, getRuntimeSnapshot, openDesktopPath, pickScanFolders } from "./desktop"; +import { SetupScreen } from "./components/SetupScreen"; +import { ScanningScreen } from "./components/ScanningScreen"; +import { SummaryScreen } from "./components/SummaryScreen"; +import { ClusterList } from "./components/ClusterList"; +import { DiffWorkspace } from "./components/DiffWorkspace"; +import { FinalizeDeletionScreen } from "./components/FinalizeDeletionScreen"; +import { WorkflowRail } from "./components/WorkflowRail"; +import { buildDeletionWorkflowModel, getActiveKeeperId, hasExplicitKeeperDecision, mergeDeleteAttemptResults } from "./utils"; +import { antTheme } from "./theme/antdTheme"; +import { getWorkflowStepState } from "./workflow"; + +function normalizeFolderPaths(entries) { + return entries.map((entry) => entry.path.trim()).filter(Boolean); +} + +function mergeFolderInputs(currentEntries, nextPaths) { + const uniqueNextPaths = Array.from(new Set(nextPaths.map((item) => item.trim()).filter(Boolean))); + if (!uniqueNextPaths.length) return currentEntries; + const nextEntries = currentEntries.map((entry) => ({ ...entry })); + const existingPaths = new Set(nextEntries.map((entry) => entry.path.trim()).filter(Boolean)); + uniqueNextPaths.forEach((path) => { + if (existingPaths.has(path)) return; + const emptyEntry = nextEntries.find((entry) => !entry.path.trim()); + if (emptyEntry) emptyEntry.path = path; + else nextEntries.push({ id: `folder-${Date.now()}-${Math.random()}`, path }); + existingPaths.add(path); + }); + return nextEntries; +} + +function AppContent() { + const antContext = AntApp.useApp(); + const [appView, setAppView] = useState("setup"); + const [form, setForm] = useState({ + folders: [{ id: "f1", path: "" }], + use_preferred_roots: true, + force_rescan: false, + clear_cache: false, + full_hash_scan: false, + bitrate_mode: "128", + gemini_enabled: false, + gemini_api_key: "", + }); + const [executingDelete, setExecutingDelete] = useState(false); + const [focusedAlbumId, setFocusedAlbumId] = useState(null); + const [runtimeInfo, setRuntimeInfo] = useState(getRuntimeSnapshot()); + const [deleteAttemptResults, setDeleteAttemptResults] = useState({}); + const [feedbackSummary, setFeedbackSummary] = useState(null); + const [geminiSettings, setGeminiSettings] = useState({ has_api_key: false }); + const d = useDeduplicator(); + + const selectedCluster = d.clusters.find((cluster) => cluster.cluster_id === d.selectedClusterId) || null; + const currentKeeperId = getActiveKeeperId(selectedCluster, d.decisions); + const hasExplicitDecision = hasExplicitKeeperDecision(selectedCluster, d.decisions); + const deletionWorkflow = buildDeletionWorkflowModel(d.allClusters, d.preview, d.decisions, deleteAttemptResults); + const workflowSteps = useMemo( + () => getWorkflowStepState({ + appView, + status: d.status, + summary: d.summary, + preview: d.preview, + progress: d.progress, + }), + [appView, d.preview, d.progress, d.status, d.summary], + ); + + useEffect(() => { + let active = true; + getRuntimeInfo().then((info) => { + if (active) setRuntimeInfo(info); + }).catch(() => {}); + return () => { active = false; }; + }, []); + + useEffect(() => { + if (d.status === "running") setAppView("scanning"); + if (d.status === "completed" && d.summary && appView === "scanning") setAppView("summary"); + }, [appView, d.status, d.summary]); + + useEffect(() => { + setFocusedAlbumId(null); + }, [d.selectedClusterId, d.selectedTab]); + + useEffect(() => { + if (!d.error) return; + antContext.notification.error({ message: "שגיאה", description: d.error, placement: "topLeft" }); + d.setError(""); + }, [antContext.notification, d]); + + useEffect(() => { + if (!d.successSummary) return; + antContext.notification.success({ message: "העברה הושלמה", description: `הועברו ${d.successSummary.moved_count} תיקיות.`, placement: "topLeft" }); + d.setSuccessSummary(null); + }, [antContext.notification, d]); + + const refreshFeedbackSummary = async () => { + try { + const summary = await api.getFeedbackSummary(); + setFeedbackSummary(summary); + } catch { + setFeedbackSummary(null); + } + }; + + useEffect(() => { + refreshFeedbackSummary(); + }, []); + + useEffect(() => { + let active = true; + api.getGeminiSettings() + .then((settings) => { + if (active) setGeminiSettings(settings); + }) + .catch(() => { + if (active) setGeminiSettings({ has_api_key: false }); + }); + return () => { active = false; }; + }, []); + + const submitScan = async () => { + d.setError(""); + d.setSuccessSummary(null); + try { + const formPayload = form; + const preferenceOrder = normalizeFolderPaths(form.folders); + const payload = { + ...formPayload, + folders: preferenceOrder, + preferred_root: form.use_preferred_roots ? preferenceOrder[0] || null : null, + preferred_roots: form.use_preferred_roots ? preferenceOrder : [], + gemini_api_key: form.gemini_api_key.trim() || null, + }; + const created = await api.createAnalysisSession(payload); + d.setSessionId(created.session_id); + d.setStatus(created.status); + d.setSummary(null); + d.setProgress({ + step: "queued", + stage: "queued", + message: "ממתין", + human_message: "ממתין לתחילת הניתוח.", + current: 0, + total: 1, + percent: 0, + warnings: [], + }); + d.setClusters([]); + d.setDecisions({}); + d.setDeleteSelections({}); + d.setPreview({ items: [], total_count: 0, total_size_mb: 0, auto_selected_count: 0, manual_selected_count: 0 }); + d.setAllClusters([]); + d.setSelectedClusterId(null); + setDeleteAttemptResults({}); + await refreshFeedbackSummary(); + setAppView("scanning"); + } catch (err) { + d.setError(err.message); + } + }; + + const handleScanSubmit = async (event) => { + if (event) event.preventDefault(); + + const existingClusterCount = (d.summary?.counts?.safe_clusters ?? 0) + (d.summary?.counts?.review_clusters ?? 0); + const hasExistingResults = d.status === "completed" && ( + Boolean(d.summary) + || d.allClusters.length > 0 + || d.preview.total_count > 0 + ); + + if (!hasExistingResults) { + await submitScan(); + return; + } + + antContext.modal.confirm({ + centered: true, + title: "להתחיל סריקה חדשה במקום התוצאות הקיימות?", + icon: , + okText: "כן, התחל מחדש", + cancelText: "ביטול", + content: ( +
+ יש כבר תוצאות סריקה שמוכנות לעבודה בחלון הזה. + + {existingClusterCount > 0 + ? `סריקה חדשה תאפס ${existingClusterCount} קבוצות שנמצאו ואת כל סימוני השמירה או ההעברה שביצעת עד כה.` + : "סריקה חדשה תאפס את הממצאים והסימונים הקיימים בסשן הנוכחי."} + + התהליך עשוי לארוך זמן בהתאם לגודל הספרייה ולבדיקות שסימנת. +
+ ), + onOk: () => submitScan(), + }); + }; + + const executeDelete = async (folderIdsToExecute = null) => { + if (!d.sessionId) return; + const targetIds = folderIdsToExecute || d.preview.items.map(item => item.folder_id); + if(targetIds.length === 0) return; + + setExecutingDelete(true); + try { + const execution = await api.executeDelete(d.sessionId, targetIds); + setDeleteAttemptResults((prev) => mergeDeleteAttemptResults(prev, execution.results)); + await d.refreshData(d.sessionId, d.selectedTab); + await refreshFeedbackSummary(); + if(appView === "finalize") setAppView("review"); // Return to IDE mode after mass delete + } catch (err) { + d.setError(err.message); + } finally { + setExecutingDelete(false); + } + }; + + const updateClusterDecision = async (clusterId, keeperId) => { + await d.handleDecision(clusterId, keeperId, null); + }; + + const clearClusterDecision = async (clusterId) => { + await d.handleDecision(clusterId, null, []); + await refreshFeedbackSummary(); + }; + + const exportFeedbackData = async () => { + if (!feedbackSummary?.export_url) return; + try { + const result = await exportFeedbackFile(api.buildApiUrl(feedbackSummary.export_url)); + if (result?.canceled) return; + antContext.notification.success({ + title: "נתוני האימון יוצאו", + description: result?.filePath ? `הקובץ נשמר: ${result.filePath}` : "קובץ ה-JSONL מוכן לשיתוף.", + placement: "topLeft", + }); + } catch (err) { + d.setError(err.message); + } + }; + + const clearFeedbackHistory = async () => { + try { + const summary = await api.clearFeedbackHistory(); + setFeedbackSummary(summary); + antContext.notification.success({ + title: "היסטוריית הזיהויים נוקתה", + description: "קובץ נתוני האימון המקומי נמחק.", + placement: "topLeft", + }); + } catch (err) { + d.setError(err.message); + } + }; + + const navigateToSetup = () => { + d.setError(""); + d.setSuccessSummary(null); + setAppView("setup"); + }; + + const resetForNewScan = () => { + d.resetSession(); + setDeleteAttemptResults({}); + setAppView("setup"); + }; + + const handleWorkflowNavigate = (nextView) => { + if (nextView === "setup") { + navigateToSetup(); + return; + } + setAppView(nextView); + }; + + const openExplorer = async (path) => { + try { + const opened = await openDesktopPath(path); + if (!opened) await api.openInExplorer(path); + } catch (err) { d.setError(err.message); } + }; + + const handlePickFolders = async () => { + const paths = await pickScanFolders(); + if (paths.length) setForm((prev) => ({ ...prev, folders: mergeFolderInputs(prev.folders, paths) })); + }; + + const handlePickFolder = async (folderId, currentPath = "") => { + const paths = await pickScanFolders({ allowMultiple: false, defaultPath: currentPath || undefined }); + const nextPath = paths[0]; + if (!nextPath) return; + setForm((prev) => ({ + ...prev, + folders: prev.folders.map((folder) => (folder.id === folderId ? { ...folder, path: nextPath } : folder)), + })); + }; + + useKeyboardShortcuts({ + appView, status: d.status, clusters: d.clusters, selectedCluster, selectedClusterId: d.selectedClusterId, + setSelectedClusterId: d.setSelectedClusterId, currentKeeperId, focusedAlbumId, setFocusedAlbumId, + handleDecision: updateClusterDecision, preview: d.preview, openFinalize: () => setAppView("finalize"), + openExplorer, + }); + + return ( + + +
+ +
+ {appView === "setup" && ( + + )} + {appView === "scanning" && } + {appView === "summary" && d.summary && ( + setAppView("review")} + /> + )} + {appView === "review" && d.status === "completed" && ( +
+ +
+ setAppView("finalize")} + onExecuteMassDelete={() => executeDelete()} + isExecuting={executingDelete} + /> +
+
+ )} + {appView === "finalize" && d.status === "completed" && ( + + )} +
+
+
+
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/album_deduplicator/frontend/src/App.reviewLayout.test.jsx b/album_deduplicator/frontend/src/App.reviewLayout.test.jsx new file mode 100644 index 0000000..07c7d9e --- /dev/null +++ b/album_deduplicator/frontend/src/App.reviewLayout.test.jsx @@ -0,0 +1,207 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import App from "./App"; + +class MockEventSource { + static instances = []; + + constructor(url) { + this.url = url; + this.listeners = new Map(); + MockEventSource.instances.push(this); + } + + addEventListener(name, listener) { + const current = this.listeners.get(name) ?? []; + current.push(listener); + this.listeners.set(name, current); + } + + emit(name, data = {}) { + const listeners = this.listeners.get(name) ?? []; + listeners.forEach((listener) => listener({ data: JSON.stringify(data) })); + } + + close() {} +} + +function jsonResponse(payload) { + return { + ok: true, + status: 200, + json: async () => payload, + text: async () => JSON.stringify(payload), + }; +} + +const sessionSummary = { + session_id: "session-1", + status: "completed", + progress: { + step: "completed", + stage: "complete", + message: "הניתוח הושלם", + human_message: "הניתוח הסתיים. אפשר להתחיל לעבור על הקבוצות.", + current: 1, + total: 1, + percent: 100, + warnings: [], + }, + counts: { + folders: 2, + compared_pairs: 1, + safe_clusters: 1, + review_clusters: 0, + }, + error: null, +}; + +const clusterResponse = { + clusters: [ + { + cluster_id: "cluster-1", + confidence_bucket: "safe", + recommended_keeper_id: "folder-keep", + selected_delete_folder_ids: ["folder-drop"], + human_summary: 'נמצאו עותקים כמעט זהים. מומלץ לשמור את "Best".', + resolution_state: "auto", + albums: [ + { + folder_id: "folder-keep", + path: "C:/Music/Best", + name: "Best", + quality_score: 95, + avg_bitrate: 320, + file_count: 10, + total_size_mb: 50, + is_deleted: false, + album_art_preview_url: "/api/analysis-sessions/session-1/albums/folder-keep/cover", + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "C:/Music/Best/01.mp3", + title: "Song A", + duration: 180, + bitrate: 320, + stream_url: "/api/analysis-sessions/session-1/albums/folder-keep/tracks/0/stream", + }, + ], + }, + { + folder_id: "folder-drop", + path: "D:/Archive/Best", + name: "Archive Copy", + quality_score: 84, + avg_bitrate: 192, + file_count: 10, + total_size_mb: 45, + is_deleted: false, + album_art_preview_url: null, + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "D:/Archive/Best/01.mp3", + title: "Song A", + duration: 180, + bitrate: 192, + stream_url: "/api/analysis-sessions/session-1/albums/folder-drop/tracks/0/stream", + }, + ], + }, + ], + pairs: [ + { + pair_id: "pair-1", + folder1_id: "folder-keep", + folder2_id: "folder-drop", + algorithmic_score: 98, + ml_score: 99, + base_score: 98.5, + gemini_score: null, + final_score: 98.5, + is_identical_by_hash: false, + }, + ], + }, + ], +}; + +const previewResponse = { + items: [ + { + folder_id: "folder-drop", + folder_path: "D:/Archive/Best", + folder_name: "Archive Copy", + keeper_folder_id: "folder-keep", + keeper_folder_name: "Best", + keeper_folder_path: "C:/Music/Best", + cluster_id: "cluster-1", + estimated_size_mb: 45, + selection_source: "auto", + }, + ], + total_count: 1, + total_size_mb: 45, + auto_selected_count: 1, + manual_selected_count: 0, +}; + +describe("App review layout", () => { + beforeEach(() => { + MockEventSource.instances = []; + vi.stubGlobal("EventSource", MockEventSource); + }); + + afterEach(() => { + cleanup(); + delete window.albumDeduplicator; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("keeps the review workspace layout stable and exposes the transfer step", async () => { + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + const folderInputs = screen.getAllByRole("textbox", { name: /תיקייה לסריקה/i }); + + fireEvent.change(folderInputs[0], { target: { value: "C:\\Music" } }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" }), { target: { value: "D:\\Archive" } }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => + expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "פתח סביבת עבודה" })); + + await waitFor(() => expect(screen.getByTestId("review-workspace")).toBeInTheDocument()); + + expect(screen.getByTestId("cluster-scroll")).toBeInTheDocument(); + expect(screen.getByTestId("review-main")).toBeInTheDocument(); + expect(screen.getByTestId("diff-shell")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "עבור לשלב ההעברה" })).toBeInTheDocument(); + }, 10000); +}); diff --git a/album_deduplicator/frontend/src/App.test.jsx b/album_deduplicator/frontend/src/App.test.jsx new file mode 100644 index 0000000..e2228f0 --- /dev/null +++ b/album_deduplicator/frontend/src/App.test.jsx @@ -0,0 +1,1039 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import App from "./App"; + +class MockEventSource { + static instances = []; + + constructor(url) { + this.url = url; + this.listeners = new Map(); + MockEventSource.instances.push(this); + } + + addEventListener(name, listener) { + const current = this.listeners.get(name) ?? []; + current.push(listener); + this.listeners.set(name, current); + } + + emit(name, data = {}) { + const listeners = this.listeners.get(name) ?? []; + listeners.forEach((listener) => listener({ data: JSON.stringify(data) })); + } + + close() {} +} + +function jsonResponse(payload) { + return { + ok: true, + status: 200, + json: async () => payload, + text: async () => JSON.stringify(payload), + }; +} + +function createDesktopBridge(overrides = {}) { + return { + runtime: { + isElectron: true, + backendBaseUrl: "http://127.0.0.1:9900", + version: "0.1.0", + platform: "win32", + }, + getRuntimeInfo: vi.fn(async () => ({ + isElectron: true, + backendBaseUrl: "http://127.0.0.1:9900", + version: "0.1.0", + platform: "win32", + })), + selectScanFolders: vi.fn(async () => []), + selectPreferredRoot: vi.fn(async () => null), + openPath: vi.fn(async () => ({ ok: true })), + revealPath: vi.fn(async () => ({ ok: true })), + exportFeedback: vi.fn(async () => ({ ok: true, filePath: "C:\\Exports\\album-deduplicator-user-feedback.jsonl" })), + ...overrides, + }; +} + +const sessionSummary = { + session_id: "session-1", + status: "completed", + progress: { + step: "completed", + stage: "complete", + message: "הניתוח הושלם", + human_message: "הניתוח הסתיים. אפשר להתחיל לעבור על הקבוצות.", + current: 1, + total: 1, + percent: 100, + warnings: [], + }, + counts: { + folders: 2, + compared_pairs: 1, + safe_clusters: 1, + review_clusters: 0, + }, + error: null, +}; + +const clusterResponse = { + clusters: [ + { + cluster_id: "cluster-1", + confidence_bucket: "safe", + recommended_keeper_id: "folder-keep", + selected_delete_folder_ids: ["folder-drop"], + human_summary: 'נמצאו עותקים כמעט זהים. מומלץ לשמור את "Best".', + resolution_state: "auto", + albums: [ + { + folder_id: "folder-keep", + path: "C:/Music/Best", + name: "Best", + quality_score: 95, + avg_bitrate: 320, + file_count: 10, + total_size_mb: 50, + is_deleted: false, + album_art_preview_url: "/api/analysis-sessions/session-1/albums/folder-keep/cover", + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "C:/Music/Best/01.mp3", + title: "Song A", + duration: 180, + bitrate: 320, + stream_url: "/api/analysis-sessions/session-1/albums/folder-keep/tracks/0/stream", + }, + ], + }, + { + folder_id: "folder-drop", + path: "D:/Archive/Best", + name: "Archive Copy", + quality_score: 84, + avg_bitrate: 192, + file_count: 10, + total_size_mb: 45, + is_deleted: false, + album_art_preview_url: null, + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "D:/Archive/Best/01.mp3", + title: "Song A", + duration: 180, + bitrate: 192, + stream_url: "/api/analysis-sessions/session-1/albums/folder-drop/tracks/0/stream", + }, + ], + }, + ], + pairs: [ + { + pair_id: "pair-1", + folder1_id: "folder-keep", + folder2_id: "folder-drop", + algorithmic_score: 98, + ml_score: 99, + base_score: 98.5, + gemini_score: null, + final_score: 98.5, + is_identical_by_hash: false, + }, + ], + }, + ], +}; + +const previewResponse = { + items: [ + { + folder_id: "folder-drop", + folder_path: "D:/Archive/Best", + folder_name: "Archive Copy", + keeper_folder_id: "folder-keep", + keeper_folder_name: "Best", + keeper_folder_path: "C:/Music/Best", + cluster_id: "cluster-1", + estimated_size_mb: 45, + selection_source: "auto", + }, + ], + total_count: 1, + total_size_mb: 45, + auto_selected_count: 1, + manual_selected_count: 0, +}; + +const emptyPreviewResponse = { + items: [], + total_count: 0, + total_size_mb: 0, + auto_selected_count: 0, + manual_selected_count: 0, +}; + +const feedbackSummaryResponse = { + feedback_file_path: "C:/Users/me/AppData/Local/Music Automatic/Album Deduplicator/user_feedback/user_feedback_events.jsonl", + event_count: 3, + size_bytes: 1024, + export_url: "/api/ml-feedback/export", +}; + +function getScanFolderInputs() { + return screen.getAllByRole("textbox", { name: /תיקייה לסריקה/i }); +} + +function getWorkflowStep(key) { + return screen.getByTestId(`workflow-step-${key}`).closest(".ant-steps-item"); +} + +describe("App", () => { + beforeEach(() => { + MockEventSource.instances = []; + vi.stubGlobal("EventSource", MockEventSource); + window.localStorage.clear(); + window.sessionStorage.clear(); + delete window.albumDeduplicator; + }); + + afterEach(() => { + cleanup(); + delete window.albumDeduplicator; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("renders the compact setup flow with the restored settings options", () => { + window.albumDeduplicator = createDesktopBridge(); + vi.stubGlobal("fetch", vi.fn()); + + render(); + + expect(screen.getByTestId("workflow-rail")).toBeInTheDocument(); + expect(getWorkflowStep("setup")).not.toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("scanning")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("summary")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("review")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("finalize")).toHaveClass("ant-steps-item-disabled"); + expect(screen.getByText("מיוזיק אוטומטיק")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "בחר תיקייה עבור שורה 1" })).toBeInTheDocument(); + expect(screen.queryByRole("textbox", { name: "תיקייה לסריקה 2" })).not.toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "הפעל סדר עדיפות לשמירה" })).toBeChecked(); + expect(screen.getByText("סדר השורות הוא סדר ההעדפה: תיקייה עליונה עדיפה על זו שמתחתיה, גם עבור תתי-תיקיות שנמצאות בתוכה.")).toBeInTheDocument(); + expect(screen.queryByText("כאשר נמצאים עותקים זהים או באותה איכות, המערכת תשמור את העותק שנמצא תחת התיקייה שמופיעה מוקדם יותר בסדר.")).not.toBeInTheDocument(); + fireEvent.click(screen.getByText("הגדרות מתקדמות")); + expect(screen.getByRole("checkbox", { name: "רענון מלא מהדיסק" })).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "בדיקת Hash מלאה" })).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "אימות AI למקרים גבוליים" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "התחל סריקה" })).toBeDisabled(); + }); + + it("submits full hash scan when the toggle is enabled", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse({ clusters: [] }); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse({ + ...sessionSummary, + counts: { + ...sessionSummary.counts, + safe_clusters: 0, + review_clusters: 0, + }, + }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + const folderInputs = getScanFolderInputs(); + fireEvent.change(folderInputs[0], { target: { value: "C:\\Music" } }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" }), { target: { value: "D:\\Archive" } }); + fireEvent.click(screen.getByText("הגדרות מתקדמות")); + fireEvent.click(screen.getByRole("checkbox", { name: "בדיקת Hash מלאה" })); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => { + const createRequest = fetchMock.mock.calls.find(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + ); + expect(createRequest).toBeTruthy(); + expect(JSON.parse(createRequest[1].body)).toMatchObject({ + folders: ["C:\\Music", "D:\\Archive"], + preferred_root: "C:\\Music", + preferred_roots: ["C:\\Music", "D:\\Archive"], + full_hash_scan: true, + }); + }); + }); + + it("loads saved Gemini key status and submits a new key with the scan", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/settings/gemini")) { + return jsonResponse({ has_api_key: true }); + } + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse({ clusters: [] }); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse({ + ...sessionSummary, + counts: { + ...sessionSummary.counts, + safe_clusters: 0, + review_clusters: 0, + }, + }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByText("הגדרות מתקדמות")); + + expect(await screen.findByText("מפתח Gemini שמור במחשב הזה. אפשר להשאיר ריק כדי להשתמש בו.")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("checkbox", { name: "אימות AI למקרים גבוליים" })); + fireEvent.change(screen.getByLabelText("מפתח API של Gemini"), { + target: { value: "new-gemini-key" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => { + const createRequest = fetchMock.mock.calls.find(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + ); + expect(createRequest).toBeTruthy(); + expect(JSON.parse(createRequest[1].body)).toMatchObject({ + gemini_enabled: true, + gemini_api_key: "new-gemini-key", + }); + }); + }); + + it("uses the electron bridge to pick scan folders without duplicating an existing path", async () => { + window.albumDeduplicator = createDesktopBridge({ + selectScanFolders: vi.fn(async () => ["C:\\Music", "D:\\Archive", "E:\\Collection"]), + }); + vi.stubGlobal("fetch", vi.fn()); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + + fireEvent.click(screen.getByRole("button", { name: /הוספת כמה תיקיות/ })); + + await waitFor(() => { + expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" })).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 3" })).toBeInTheDocument(); + }); + + await waitFor(() => { + const pathInputs = getScanFolderInputs(); + expect(pathInputs[0]).toHaveValue("C:\\Music"); + expect(pathInputs[1]).toHaveValue("D:\\Archive"); + expect(pathInputs[2]).toHaveValue("E:\\Collection"); + }); + }); + + it("fills a single scan row from the folder picker button", async () => { + const selectScanFolders = vi.fn(async () => ["D:\\Archive"]); + window.albumDeduplicator = createDesktopBridge({ selectScanFolders }); + vi.stubGlobal("fetch", vi.fn()); + + render(); + + fireEvent.click(screen.getByRole("button", { name: "בחר תיקייה עבור שורה 1" })); + + await waitFor(() => { + expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toHaveValue("D:\\Archive"); + }); + + expect(selectScanFolders).toHaveBeenCalledWith({ allowMultiple: false, defaultPath: undefined }); + }); + + it("submits the configured preference order from the existing scan rows", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse({ clusters: [] }); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse({ + ...sessionSummary, + counts: { + ...sessionSummary.counts, + safe_clusters: 0, + review_clusters: 0, + }, + }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" }), { + target: { value: "D:\\Archive" }, + }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 3" }), { + target: { value: "E:\\Backup" }, + }); + + fireEvent.click(screen.getByRole("button", { name: "העלה את תיקייה 2 בסדר ההעדפה" })); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => { + const createRequest = fetchMock.mock.calls.find(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + ); + expect(createRequest).toBeTruthy(); + expect(JSON.parse(createRequest[1].body)).toMatchObject({ + preferred_root: "D:\\Archive", + preferred_roots: ["D:\\Archive", "C:\\Music", "E:\\Backup"], + use_preferred_roots: true, + }); + }); + }); + + it("can submit a scan with preference order disabled", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse({ clusters: [] }); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse({ + ...sessionSummary, + counts: { + ...sessionSummary.counts, + safe_clusters: 0, + review_clusters: 0, + }, + }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" }), { + target: { value: "D:\\Archive" }, + }); + + fireEvent.click(screen.getByRole("checkbox", { name: "הפעל סדר עדיפות לשמירה" })); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => { + const createRequest = fetchMock.mock.calls.find(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + ); + expect(createRequest).toBeTruthy(); + expect(JSON.parse(createRequest[1].body)).toMatchObject({ + preferred_root: null, + preferred_roots: [], + use_preferred_roots: false, + }); + }); + }); + + it("opens the finalize screen and requires confirmation before delete", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-executions")) { + return jsonResponse({ + moved_count: 1, + failed_count: 0, + total_size_mb: 45, + results: [], + }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + const folderInputs = getScanFolderInputs(); + fireEvent.change(folderInputs[0], { target: { value: "C:\\Music" } }); + fireEvent.click(screen.getByRole("button", { name: /הוסף תיקייה נוספת/ })); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 2" }), { target: { value: "D:\\Archive" } }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => + expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "פתח סביבת עבודה" })); + + await waitFor(() => + expect(screen.getByRole("button", { name: "עבור לשלב ההעברה" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "עבור לשלב ההעברה" })); + + await waitFor(() => + expect(screen.getByRole("heading", { name: "אישור העברה לסל המחזור (1 תיקיות)" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "בצע מחיקה למסומנים" })); + + expect(await screen.findByText("להעביר את הפריטים המסומנים לסל המחזור?")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "כן, להעביר" })).toBeInTheDocument(); + }, 15000); + + it("selects the matching folder immediately from numeric review shortcuts", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/decisions") && options.method === "POST") { + return jsonResponse({ + ...previewResponse, + items: [{ ...previewResponse.items[0], folder_id: "folder-keep", keeper_folder_id: "folder-drop" }], + manual_selected_count: 1, + }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: "פתח סביבת עבודה" })); + + await waitFor(() => expect(screen.getByTestId("review-workspace")).toBeInTheDocument()); + fireEvent.keyDown(window, { key: "2" }); + + await waitFor(() => { + const decisionRequest = fetchMock.mock.calls.find(([url, options = {}]) => + String(url).includes("/api/analysis-sessions/session-1/decisions") && options.method === "POST" + ); + expect(decisionRequest).toBeTruthy(); + expect(JSON.parse(decisionRequest[1].body)).toEqual({ + decisions: [ + { + cluster_id: "cluster-1", + keeper_id: "folder-drop", + delete_folder_ids: ["folder-keep"], + }, + ], + }); + }); + }, 15000); + + it("clears the review decision from the space key shortcut", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/decisions") && options.method === "POST") { + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: "פתח סביבת עבודה" })); + + await waitFor(() => expect(screen.getByTestId("review-workspace")).toBeInTheDocument()); + fireEvent.keyDown(window, { key: " " }); + + await waitFor(() => { + const decisionRequest = fetchMock.mock.calls.find(([url, options = {}]) => + String(url).includes("/api/analysis-sessions/session-1/decisions") && options.method === "POST" + ); + expect(decisionRequest).toBeTruthy(); + expect(JSON.parse(decisionRequest[1].body)).toEqual({ + decisions: [ + { + cluster_id: "cluster-1", + keeper_id: null, + delete_folder_ids: [], + }, + ], + }); + }); + }, 15000); + + it("exports feedback through the Electron bridge without opening a blank child window", async () => { + const exportFeedback = vi.fn(async () => ({ ok: true, filePath: "C:\\Exports\\feedback.jsonl" })); + window.albumDeduplicator = createDesktopBridge({ exportFeedback }); + const openMock = vi.fn(); + vi.stubGlobal("open", openMock); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).includes("/api/ml-feedback/summary")) { + return jsonResponse(feedbackSummaryResponse); + } + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + + await waitFor(() => expect(screen.getByRole("button", { name: "יצא נתונים לשיתוף" })).toBeEnabled()); + fireEvent.click(screen.getByRole("button", { name: "יצא נתונים לשיתוף" })); + + await waitFor(() => { + expect(exportFeedback).toHaveBeenCalledWith("http://127.0.0.1:9900/api/ml-feedback/export"); + }); + expect(openMock).not.toHaveBeenCalled(); + }, 15000); + + it("clears feedback history from the finalize screen after confirmation", async () => { + window.albumDeduplicator = createDesktopBridge(); + let feedbackSummary = feedbackSummaryResponse; + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).includes("/api/ml-feedback/summary")) { + return jsonResponse(feedbackSummary); + } + if (String(url).includes("/api/ml-feedback") && options.method === "DELETE") { + feedbackSummary = { ...feedbackSummaryResponse, event_count: 0, size_bytes: 0 }; + return jsonResponse(feedbackSummary); + } + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + + await waitFor(() => expect(screen.getByText("3 אירועי אימון נשמרו")).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: "נקה היסטוריה" })); + + expect(await screen.findByText("לנקות את היסטוריית הזיהויים?")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "כן, נקה" })); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:9900/api/ml-feedback", expect.objectContaining({ method: "DELETE" })); + expect(screen.getByText("0 אירועי אימון נשמרו")).toBeInTheDocument(); + }); + }, 15000); + + it("keeps the finalize screen stable when canceling a pending transfer for a cluster", async () => { + window.albumDeduplicator = createDesktopBridge(); + let previewAfterDecision = previewResponse; + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/decisions") && options.method === "POST") { + previewAfterDecision = emptyPreviewResponse; + return jsonResponse(emptyPreviewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewAfterDecision); + } + if (String(url).includes("/api/analysis-sessions/session-1") && (!options.method || options.method === "GET")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + + await waitFor(() => + expect(screen.getByRole("button", { name: "בטל העברה ושמור הכל" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "בטל העברה ושמור הכל" })); + + await waitFor(() => + expect(screen.getByText("אין פריטים להעברה.")).toBeInTheDocument(), + ); + expect(screen.getByRole("heading", { name: "אישור העברה לסל המחזור (0 תיקיות)" })).toBeInTheDocument(); + }, 15000); + + it("shows the scan step as active while a session is running and keeps future steps disabled", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("progress", { + stage: "compare", + message: "משווה בין אלבומים", + human_message: "המערכת משווה בין העותקים שנמצאו.", + current: 4, + total: 10, + percent: 40, + warnings: [], + }); + + await waitFor(() => expect(screen.getByRole("heading", { name: "סריקה בתהליך" })).toBeInTheDocument()); + + expect(getWorkflowStep("setup")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("scanning")).toHaveClass("ant-steps-item-active"); + expect(getWorkflowStep("summary")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("review")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("finalize")).toHaveClass("ant-steps-item-disabled"); + expect(screen.getByTestId("workflow-step-badge-scanning")).toHaveTextContent("90%"); + }); + + it("allows jumping between completed views without clearing the current session", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + expect(screen.getByText("1 בטוחות")).toBeInTheDocument(); + expect(screen.getByText("0 לסקירה")).toBeInTheDocument(); + expect(screen.getByText("1 למחיקה")).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("workflow-step-review")); + await waitFor(() => expect(screen.getByTestId("review-workspace")).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + await waitFor(() => + expect(screen.getByRole("heading", { name: "אישור העברה לסל המחזור (1 תיקיות)" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId("workflow-step-setup")); + await waitFor(() => expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toBeInTheDocument()); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "E:\\Updated Music" }, + }); + + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + await waitFor(() => + expect(screen.getByRole("heading", { name: "אישור העברה לסל המחזור (1 תיקיות)" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId("workflow-step-review")); + await waitFor(() => expect(screen.getByRole("button", { name: "עבור לשלב ההעברה" })).toBeInTheDocument()); + }); + + it("keeps the old session available while editing setup and resets it only after submitting a new scan", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + const body = JSON.parse(options.body); + if (body.folders[0] === "E:\\Fresh Scan") { + return jsonResponse({ session_id: "session-2", status: "queued" }); + } + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId("workflow-step-finalize")); + await waitFor(() => + expect(screen.getByRole("heading", { name: "אישור העברה לסל המחזור (1 תיקיות)" })).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId("workflow-step-setup")); + await waitFor(() => expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toBeInTheDocument()); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "E:\\Fresh Scan" }, + }); + + fireEvent.click(screen.getByTestId("workflow-step-review")); + await waitFor(() => expect(screen.getByTestId("review-workspace")).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId("workflow-step-setup")); + await waitFor(() => expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toHaveValue("E:\\Fresh Scan")); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + expect((await screen.findAllByText("להתחיל סריקה חדשה במקום התוצאות הקיימות?")).length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("button", { name: "כן, התחל מחדש" })); + + await waitFor(() => expect(screen.getByRole("heading", { name: "סריקה בתהליך" })).toBeInTheDocument()); + + expect(getWorkflowStep("scanning")).toHaveClass("ant-steps-item-active"); + expect(getWorkflowStep("summary")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("review")).toHaveClass("ant-steps-item-disabled"); + expect(getWorkflowStep("finalize")).toHaveClass("ant-steps-item-disabled"); + }); + + it("warns before replacing an existing completed scan and only restarts after confirmation", async () => { + window.albumDeduplicator = createDesktopBridge(); + const fetchMock = vi.fn(async (url, options = {}) => { + if (String(url).endsWith("/api/analysis-sessions") && options.method === "POST") { + const body = JSON.parse(options.body); + if (body.folders[0] === "E:\\Fresh Scan") { + return jsonResponse({ session_id: "session-2", status: "queued" }); + } + return jsonResponse({ session_id: "session-1", status: "queued" }); + } + if (String(url).includes("/api/analysis-sessions/session-1/clusters")) { + return jsonResponse(clusterResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1/delete-preview")) { + return jsonResponse(previewResponse); + } + if (String(url).includes("/api/analysis-sessions/session-1")) { + return jsonResponse(sessionSummary); + } + throw new Error(`Unhandled fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "C:\\Music" }, + }); + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + await waitFor(() => expect(MockEventSource.instances).toHaveLength(1)); + MockEventSource.instances[0].emit("completed", { status: "completed" }); + await waitFor(() => expect(screen.getByRole("button", { name: "פתח סביבת עבודה" })).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId("workflow-step-setup")); + await waitFor(() => expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toBeInTheDocument()); + fireEvent.change(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" }), { + target: { value: "E:\\Fresh Scan" }, + }); + + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + + expect((await screen.findAllByText("להתחיל סריקה חדשה במקום התוצאות הקיימות?")).length).toBeGreaterThan(0); + expect(screen.getByText("סריקה חדשה תאפס 1 קבוצות שנמצאו ואת כל סימוני השמירה או ההעברה שביצעת עד כה.")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "ביטול" })); + + expect(screen.getByRole("textbox", { name: "תיקייה לסריקה 1" })).toHaveValue("E:\\Fresh Scan"); + expect(fetchMock.mock.calls.filter(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + )).toHaveLength(1); + + fireEvent.click(screen.getByRole("button", { name: "התחל סריקה" })); + expect((await screen.findAllByText("להתחיל סריקה חדשה במקום התוצאות הקיימות?")).length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("button", { name: "כן, התחל מחדש" })); + + await waitFor(() => expect(screen.getByRole("heading", { name: "סריקה בתהליך" })).toBeInTheDocument()); + expect(fetchMock.mock.calls.filter(([url, options]) => + String(url).endsWith("/api/analysis-sessions") && options?.method === "POST", + )).toHaveLength(2); + }); +}); diff --git a/album_deduplicator/frontend/src/api.js b/album_deduplicator/frontend/src/api.js new file mode 100644 index 0000000..741f148 --- /dev/null +++ b/album_deduplicator/frontend/src/api.js @@ -0,0 +1,75 @@ +import { getRuntimeSnapshot } from "./desktop"; + +export function buildApiUrl(path) { + if (!path) return null; + if (/^https?:\/\//i.test(path)) return path; + const apiBase = getRuntimeSnapshot().backendBaseUrl; + return `${apiBase}${path}`; +} + +export async function requestJson(path, options = {}) { + const response = await fetch(buildApiUrl(path), { + headers: { + "Content-Type": "application/json", + ...(options.headers ?? {}), + }, + ...options, + }); + if (!response.ok) { + const detail = await response.text(); + throw new Error(detail || `Request failed: ${response.status}`); + } + if (response.status === 204) return null; + return response.json(); +} +export function getEventSource(sessionId) { + return new EventSource(buildApiUrl(`/api/analysis-sessions/${sessionId}/events`)); +} +export function createAnalysisSession(payload) { + return requestJson("/api/analysis-sessions", { + method: "POST", + body: JSON.stringify(payload), + }); +} +export function getAnalysisSession(sessionId) { + return requestJson(`/api/analysis-sessions/${sessionId}`); +} +export function getClusters(sessionId, bucket) { + return requestJson(`/api/analysis-sessions/${sessionId}/clusters?bucket=${bucket}`); +} +export function getDeletePreview(sessionId) { + return requestJson(`/api/analysis-sessions/${sessionId}/delete-preview`); +} +export function getFeedbackSummary() { + return requestJson("/api/ml-feedback/summary"); +} +export function getGeminiSettings() { + return requestJson("/api/settings/gemini"); +} +export function clearFeedbackHistory() { + return requestJson("/api/ml-feedback", { method: "DELETE" }); +} +export function updateDecisions(sessionId, payload) { + return requestJson(`/api/analysis-sessions/${sessionId}/decisions`, { + method: "POST", + body: JSON.stringify(payload), + }); +} +export function executeDelete(sessionId, folderIds) { + return requestJson(`/api/analysis-sessions/${sessionId}/delete-executions`, { + method: "POST", + body: JSON.stringify({ folder_ids: folderIds }), + }); +} +export function executeSingleDelete(sessionId, clusterId, folderId) { + return requestJson(`/api/analysis-sessions/${sessionId}/delete-single`, { + method: "POST", + body: JSON.stringify({ cluster_id: clusterId, folder_id: folderId }), + }); +} +export function openInExplorer(path) { + return requestJson("/api/system/open-explorer", { + method: "POST", + body: JSON.stringify({ path }), + }); +} diff --git a/album_deduplicator/frontend/src/components/ClusterList.jsx b/album_deduplicator/frontend/src/components/ClusterList.jsx new file mode 100644 index 0000000..5fd8176 --- /dev/null +++ b/album_deduplicator/frontend/src/components/ClusterList.jsx @@ -0,0 +1,133 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Button, Segmented, Tooltip } from "antd"; +import { Icon, StatusTag } from "./UI"; +import { + getClusterDisplayTitle, + getClusterStatusMeta, + getVisibleAlbumCount, + hasExplicitKeeperDecision, + formatPercent, + clusterMatchesReviewTab, +} from "../utils"; + +const SEGMENT_OPTIONS = [ + { + label: בטוחים, + value: "safe", + }, + { + label: לסקירה, + value: "review", + }, + { + label: הושלם, + value: "completed", + }, +]; + +function getReadinessRank(cluster, decisions) { + const isResolved = hasExplicitKeeperDecision(cluster, decisions); + const statusMeta = getClusterStatusMeta(cluster, isResolved); + return statusMeta.tone === "success" ? 0 : 1; +} + +function formatClusterListCount(count) { + return count === 1 ? "פריט אחד" : `${count} פריטים`; +} + +export function ClusterList({ clusters, selectedClusterId, setSelectedClusterId, decisions, selectedTab, setSelectedTab }) { + const [manualOrderIds, setManualOrderIds] = useState([]); + const filteredClusters = useMemo( + () => clusters.filter((cluster) => clusterMatchesReviewTab(cluster, selectedTab)), + [clusters, selectedTab], + ); + const displayedClusters = useMemo(() => { + if (!manualOrderIds.length) return filteredClusters; + const manualOrder = new Map(manualOrderIds.map((clusterId, index) => [clusterId, index])); + return filteredClusters + .map((cluster, index) => ({ cluster, index })) + .sort((left, right) => { + const leftOrder = manualOrder.get(left.cluster.cluster_id); + const rightOrder = manualOrder.get(right.cluster.cluster_id); + if (leftOrder !== undefined && rightOrder !== undefined) return leftOrder - rightOrder; + if (leftOrder !== undefined) return -1; + if (rightOrder !== undefined) return 1; + return left.index - right.index; + }) + .map(({ cluster }) => cluster); + }, [filteredClusters, manualOrderIds]); + + useEffect(() => { + setManualOrderIds([]); + }, [selectedTab]); + + const sortByReadiness = () => { + setManualOrderIds( + filteredClusters + .map((cluster, index) => ({ cluster, index })) + .sort((left, right) => { + const rankDiff = getReadinessRank(left.cluster, decisions) - getReadinessRank(right.cluster, decisions); + return rankDiff || left.index - right.index; + }) + .map(({ cluster }) => cluster.cluster_id), + ); + }; + + return ( +
+
+ +
+ + {formatClusterListCount(filteredClusters.length)} + + + + +
+
+
+ {displayedClusters.map((cluster) => { + const isActive = cluster.cluster_id === selectedClusterId; + const isResolved = hasExplicitKeeperDecision(cluster, decisions); + const statusMeta = getClusterStatusMeta(cluster, isResolved); + const score = cluster.pairs?.[0] ? formatPercent(cluster.pairs[0].final_score) : "N/A"; + + return ( +
setSelectedClusterId(cluster.cluster_id)} + > +
+ {isResolved ? "✓ " : ""}{getClusterDisplayTitle(cluster)} +
+
+ {getVisibleAlbumCount(cluster)} עותקים + התאמה: {score} +
+
+ {statusMeta.label} +
+
+ ); + })} +
+
+ ); +} diff --git a/album_deduplicator/frontend/src/components/ClusterList.test.jsx b/album_deduplicator/frontend/src/components/ClusterList.test.jsx new file mode 100644 index 0000000..da33524 --- /dev/null +++ b/album_deduplicator/frontend/src/components/ClusterList.test.jsx @@ -0,0 +1,172 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { ClusterList } from "./ClusterList"; + +const clusters = [ + { + cluster_id: "cluster-pending", + confidence_bucket: "review", + recommended_keeper_id: "folder-a", + resolution_state: "skipped", + pairs: [ + { + pair_id: "pair-pending", + folder1_id: "folder-a", + folder2_id: "folder-b", + final_score: 88.1, + }, + ], + albums: [ + { folder_id: "folder-a", name: "Pending Copy", is_deleted: false }, + { folder_id: "folder-b", name: "Pending Copy", is_deleted: false }, + ], + }, + { + cluster_id: "cluster-ready", + confidence_bucket: "review", + recommended_keeper_id: "folder-c", + resolution_state: "manual", + pairs: [ + { + pair_id: "pair-ready", + folder1_id: "folder-c", + folder2_id: "folder-d", + final_score: 95.6, + }, + ], + albums: [ + { folder_id: "folder-c", name: "Ready Copy", is_deleted: false }, + { folder_id: "folder-d", name: "Ready Copy", is_deleted: false }, + ], + }, + { + cluster_id: "cluster-completed", + confidence_bucket: "review", + recommended_keeper_id: "folder-e", + resolution_state: "deleted", + pairs: [ + { + pair_id: "pair-completed", + folder1_id: "folder-e", + folder2_id: "folder-f", + final_score: 91.2, + }, + ], + albums: [ + { folder_id: "folder-e", name: "Completed Copy", is_deleted: false }, + { folder_id: "folder-f", name: "Completed Copy", is_deleted: true }, + ], + }, +]; + +describe("ClusterList", () => { + it("keeps the original cluster order after a cluster is marked ready", () => { + const { container } = render( + , + ); + + const items = Array.from(container.querySelectorAll(".ide-cluster-item")); + + expect(items).toHaveLength(2); + expect(items[0].textContent).toContain("Pending Copy"); + expect(items[0].textContent).toContain("ממתין לסקירה"); + expect(items[1].textContent).toContain("Ready Copy"); + expect(items[1].textContent).toContain("נבדק ומוכן"); + expect(items[1].textContent).toContain("התאמה: 95.6/100"); + expect(screen.getByTestId("cluster-scroll")).toBeInTheDocument(); + }); + + it("moves clusters with fewer than two visible albums into the completed segment", () => { + const { container, rerender } = render( + , + ); + + expect(container.textContent).toContain("Pending Copy"); + expect(container.textContent).toContain("Ready Copy"); + expect(container.textContent).not.toContain("Completed Copy"); + expect(container.textContent).not.toContain("הכל"); + expect(container.querySelector(".ide-sidebar-count").textContent).toBe("2 פריטים"); + + rerender( + , + ); + + expect(container.textContent).toContain("Completed Copy"); + expect(container.textContent).toContain("1 עותקים"); + expect(container.textContent).not.toContain("Pending Copy"); + expect(container.querySelector(".ide-sidebar-count").textContent).toBe("פריט אחד"); + }); + + it("uses distinct icons for safe and completed segments", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".anticon-safety-certificate")).toBeInTheDocument(); + expect(container.querySelector(".anticon-check-circle")).toBeInTheDocument(); + }); + + it("sorts by readiness only when the user presses the sort button", () => { + const { container, rerender } = render( + , + ); + const getItems = () => Array.from(container.querySelectorAll(".ide-cluster-item")); + + expect(getItems()[0].textContent).toContain("Pending Copy"); + + fireEvent.click(container.querySelector(".ide-sidebar-sort-button")); + + expect(getItems()[0].textContent).toContain("Ready Copy"); + expect(getItems()[1].textContent).toContain("Pending Copy"); + + rerender( + , + ); + + expect(getItems()[0].textContent).toContain("Ready Copy"); + expect(getItems()[1].textContent).toContain("Pending Copy"); + }); +}); diff --git a/album_deduplicator/frontend/src/components/DeletePreview.jsx b/album_deduplicator/frontend/src/components/DeletePreview.jsx new file mode 100644 index 0000000..01f8790 --- /dev/null +++ b/album_deduplicator/frontend/src/components/DeletePreview.jsx @@ -0,0 +1,4 @@ +// This component is no longer used in the new "Pro Tool" layout as the +// preview functionality has been merged directly into the DiffWorkspace toolbar. +// Keeping it empty or exporting null to satisfy any remaining imports. +export function DeletePreview() { return null; } \ No newline at end of file diff --git a/album_deduplicator/frontend/src/components/DiffWorkspace.jsx b/album_deduplicator/frontend/src/components/DiffWorkspace.jsx new file mode 100644 index 0000000..b95a327 --- /dev/null +++ b/album_deduplicator/frontend/src/components/DiffWorkspace.jsx @@ -0,0 +1,393 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Button, Image, Popconfirm, Tooltip } from "antd"; + +import { buildApiUrl } from "../api"; +import { buildTrackComparisonRows, formatBitrate, formatDuration, formatSizeMb, getTrackRowTone } from "../utils"; +import { Icon, StatusTag } from "./UI"; +import { ScoreTransparencyPanel } from "./ScoreTransparencyPanel"; + +function AlbumArtPreview({ album }) { + if (album.album_art_preview_url) { + return ( + {`עטיפת + ); + } + + return ( +
+ + אין עטיפה +
+ ); +} + +function AudioPreviewCard({ audioPreview, onDismiss, onPlaybackStateChange }) { + const audioRef = useRef(null); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + const [isPlaying, setIsPlaying] = useState(true); + const handledToggleRef = useRef(0); + + useEffect(() => { + setDuration(0); + setCurrentTime(0); + setIsPlaying(Boolean(audioPreview)); + handledToggleRef.current = audioPreview?.toggleRequest ?? 0; + }, [audioPreview?.key]); + + useEffect(() => { + onPlaybackStateChange?.(isPlaying); + }, [isPlaying, onPlaybackStateChange]); + + const audioSource = buildApiUrl(audioPreview?.streamUrl); + + const togglePlayback = async () => { + if (!audioRef.current) return; + if (!isPlaying) { + try { + await audioRef.current.play(); + } catch { + setIsPlaying(false); + } + return; + } + audioRef.current.pause(); + }; + + const handleSeek = (event) => { + const nextTime = Number(event.target.value); + setCurrentTime(nextTime); + if (audioRef.current) audioRef.current.currentTime = nextTime; + }; + + useEffect(() => { + if (!audioRef.current) return; + const nextToggleRequest = audioPreview?.toggleRequest ?? 0; + if (nextToggleRequest === 0 || nextToggleRequest === handledToggleRef.current) return; + handledToggleRef.current = nextToggleRequest; + togglePlayback(); + }, [audioPreview?.toggleRequest]); + + if (!audioPreview) return null; + + return ( +
+
+ ); +} + +export function DiffWorkspace({ + cluster, + currentKeeperId, + hasExplicitDecision = false, + handleDecision, + clearDecision, + openExplorer, + previewCount, + onOpenFinalize, + onExecuteMassDelete, + isExecuting, +}) { + const [audioPreview, setAudioPreview] = useState(null); + const [isAudioPlaying, setIsAudioPlaying] = useState(false); + + useEffect(() => { + setAudioPreview(null); + setIsAudioPlaying(false); + }, [cluster?.cluster_id, currentKeeperId]); + + const visibleAlbums = useMemo(() => ( + (cluster?.albums ?? []) + .filter((album) => !album.is_deleted) + .map((album, index) => ({ album, index })) + .sort((left, right) => { + if (left.album.folder_id === cluster?.recommended_keeper_id) return -1; + if (right.album.folder_id === cluster?.recommended_keeper_id) return 1; + return left.index - right.index; + }) + .map(({ album }) => album) + ), [cluster]); + + const trackRows = useMemo(() => buildTrackComparisonRows(visibleAlbums), [visibleAlbums]); + const visibleAlbumIds = useMemo(() => visibleAlbums.map((album) => album.folder_id), [visibleAlbums]); + const hasSuggestedKeeper = Boolean(currentKeeperId); + const keeperAlbum = hasExplicitDecision + ? visibleAlbums.find((album) => album.folder_id === currentKeeperId) ?? null + : null; + const suggestedKeeperAlbum = hasSuggestedKeeper + ? visibleAlbums.find((album) => album.folder_id === currentKeeperId) ?? null + : null; + const deleteCount = hasExplicitDecision && currentKeeperId ? Math.max(visibleAlbums.length - 1, 0) : 0; + + if (!cluster) { + return ( +
+ בחר קבוצה מהרשימה +
+ ); + } + + const handleTrackPreview = (album, entry) => { + if (!entry?.stream_url) return; + const nextKey = `${album.folder_id}:${entry.track_index ?? entry.filename}`; + setAudioPreview((current) => { + if (current?.key === nextKey) { + return { + ...current, + toggleRequest: (current.toggleRequest ?? 0) + 1, + }; + } + return { + key: nextKey, + trackTitle: entry.title || entry.filename, + filePath: entry.filepath || album.path, + streamUrl: entry.stream_url, + toggleRequest: 0, + }; + }); + }; + + return ( +
+
+
+ + {cluster.confidence_bucket === "safe" ? "בטוח" : "לסקירה"} + + {cluster.human_summary} + +
+
+ + נשמר: {keeperAlbum ? keeperAlbum.name : "לא נבחר"} + + {!hasExplicitDecision && suggestedKeeperAlbum ? ( + + מומלץ לשמירה: {suggestedKeeperAlbum.name} + + ) : null} + 0 ? "warning" : "neutral"} icon="trash">למחיקה: {deleteCount} + בחירה מהירה: 1-9 בוחרים עותק, רווח שומר הכל + {previewCount > 0 ? ( + <> + + + + + + ) : null} +
+
+ + {currentKeeperId ? ( +
+
+ + {keeperAlbum ? `${keeperAlbum.name} נשמר כרגע` : "קיימת כרגע הכרעת שמירה לקבוצה זו"} + + + {deleteCount > 0 + ? `${deleteCount} עותקים יסומנו למחיקה אם תמשיך לשלב ההעברה.` + : "כרגע אין פריטים שמסומנים למחיקה, אבל עדיין קיימת הכרעה פעילה לקבוצה."} + {" "}אפשר ללחוץ רווח כדי לבטל הכרעה ולשמור את כל העותקים. + +
+ +
+ ) : null} + +
+
+ {visibleAlbums.map((album, index) => { + const copyNumber = index + 1; + const isSuggestedKeeper = currentKeeperId === album.folder_id; + const isKeeper = hasExplicitDecision && isSuggestedKeeper; + const isTrash = hasExplicitDecision && Boolean(currentKeeperId) && !isKeeper; + + return ( +
+
+
+
+ + + {album.name || `עותק ${copyNumber}`} + +
+
+ {cluster.recommended_keeper_id === album.folder_id ? מומלץ : null} + +
+
+ +
+ +
+
עותק {copyNumber} · לחיצה על {copyNumber} בוחרת מיד לשמירה
+
{album.path}
+
+ ביטרייט: {formatBitrate(album.avg_bitrate)} + גודל: {formatSizeMb(album.total_size_mb)} + איכות: {album.quality_score ? `${album.quality_score.toFixed(1)}/100` : "-"} +
+
+
+
+ +
+ {trackRows.map((row) => { + const entry = row.entries[album.folder_id]; + const tone = getTrackRowTone(row, visibleAlbumIds); + const isDiff = tone === "different"; + + if (!entry) { + return
חסר בעותק זה
; + } + + const isActivePreview = audioPreview?.key === `${album.folder_id}:${entry.track_index ?? entry.filename}`; + const isPlaying = isActivePreview && isAudioPlaying; + + return ( +
+
+ {entry.filename} + {formatDuration(entry.duration)} +
+
+ ); + })} +
+ +
+ +
+
+ ); + })} +
+
+ + {audioPreview ? ( + { + setAudioPreview(null); + setIsAudioPlaying(false); + }} + onPlaybackStateChange={setIsAudioPlaying} + /> + ) : null} +
+ ); +} diff --git a/album_deduplicator/frontend/src/components/DiffWorkspace.test.jsx b/album_deduplicator/frontend/src/components/DiffWorkspace.test.jsx new file mode 100644 index 0000000..2381cc3 --- /dev/null +++ b/album_deduplicator/frontend/src/components/DiffWorkspace.test.jsx @@ -0,0 +1,275 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { DiffWorkspace } from "./DiffWorkspace"; + +const cluster = { + cluster_id: "cluster-1", + confidence_bucket: "review", + recommended_keeper_id: "folder-1", + human_summary: "יש דמיון גבוה בין העותקים, אבל נדרשת בדיקה ידנית לפני מחיקה.", + albums: [ + { + folder_id: "folder-1", + path: "C:/Music/Acoustix", + name: "Acoustix", + quality_score: 95, + avg_bitrate: 320, + file_count: 2, + album_art_preview_url: "/api/analysis-sessions/session-1/albums/folder-1/cover", + total_size_mb: 120, + is_deleted: false, + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "C:/Music/Acoustix/01.mp3", + title: "פתיחה", + duration: 180, + bitrate: 320, + stream_url: "/api/analysis-sessions/session-1/albums/folder-1/tracks/0/stream", + }, + ], + }, + { + folder_id: "folder-2", + path: "D:/Archive/Acoustix", + name: "Acoustix Archive", + quality_score: 88, + avg_bitrate: 256, + file_count: 2, + album_art_preview_url: null, + total_size_mb: 110, + is_deleted: false, + tracks: [ + { + track_index: 0, + filename: "01.mp3", + filepath: "D:/Archive/Acoustix/01.mp3", + title: "פתיחה", + duration: 180, + bitrate: 256, + stream_url: "/api/analysis-sessions/session-1/albums/folder-2/tracks/0/stream", + }, + ], + }, + ], + pairs: [ + { + pair_id: "pair-1", + folder1_id: "folder-1", + folder2_id: "folder-2", + algorithmic_score: 92, + ml_score: 94, + base_score: 93.1, + gemini_score: 90, + final_score: 92.6, + is_identical_by_hash: false, + }, + ], +}; + +describe("DiffWorkspace", () => { + beforeEach(() => { + vi.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(function mockPlay() { + this.dispatchEvent(new Event("play")); + return Promise.resolve(); + }); + vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(function mockPause() { + this.dispatchEvent(new Event("pause")); + }); + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + it("renders album art and fallback cover states in the compact review panes", () => { + render( + , + ); + + expect(screen.getByAltText("עטיפת Acoustix")).toBeInTheDocument(); + expect(screen.getByLabelText("אין עטיפה זמינה")).toBeInTheDocument(); + expect(screen.getByText("נשמר: Acoustix")).toBeInTheDocument(); + expect(screen.getByText("למחיקה: 1")).toBeInTheDocument(); + }); + + it("starts an in-app audio preview and allows pausing from the same track button", () => { + const { container } = render( + , + ); + + fireEvent.click(screen.getAllByRole("button", { name: "נגן את 01.mp3" })[0]); + + expect(screen.getByTestId("audio-preview-card")).toBeInTheDocument(); + expect(screen.getByTestId("review-scroll-shell")).toHaveClass("has-audio-preview"); + expect(screen.getByText("השמעת השוואה")).toBeInTheDocument(); + expect(screen.getByText("פתיחה")).toBeInTheDocument(); + expect(screen.getByText("C:/Music/Acoustix/01.mp3")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "סגור את נגן ההשוואה" })).toBeInTheDocument(); + expect(screen.getByLabelText("ציר הזמן של פתיחה")).toBeInTheDocument(); + expect(container.querySelector("audio")).not.toBeNull(); + + fireEvent.click(screen.getAllByRole("button", { name: "השהה את 01.mp3" })[0]); + + expect(screen.getAllByRole("button", { name: "נגן את 01.mp3" }).length).toBeGreaterThan(0); + expect(screen.queryByRole("button", { name: "השהה את 01.mp3" })).not.toBeInTheDocument(); + }); + + it("opens a confirmation before mass delete from the review toolbar", async () => { + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "העבר למחזור (2)" })); + + expect(await screen.findByText("להעביר את כל הפריטים המסומנים לסל המחזור?")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "כן, להעביר" })).toBeInTheDocument(); + }); + + it("keeps copy actions selectable when no keeper has been chosen yet", () => { + render( + , + ); + + expect(screen.getByRole("button", { name: "שמור עותק 1" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "שמור עותק 2" })).toBeInTheDocument(); + expect(screen.getByText("נשמר: לא נבחר")).toBeInTheDocument(); + expect(screen.getByText("למחיקה: 0")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "בטל הכרעה ושמור את כל העותקים" })).not.toBeInTheDocument(); + }); + + it("selects a keeper immediately from the numbered copy button", () => { + const handleDecision = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "בחר עותק 2 לשמירה" })); + + expect(handleDecision).toHaveBeenCalledWith("cluster-1", "folder-2"); + }); + + it("shows a recommended keeper in review without presenting it as a manual selection", () => { + render( + , + ); + + expect(screen.getByText("נשמר: לא נבחר")).toBeInTheDocument(); + expect(screen.getByText("מומלץ לשמירה: Acoustix")).toBeInTheDocument(); + expect(screen.getAllByRole("button", { name: "בחר עותק 1 לשמירה" }).length).toBeGreaterThan(0); + expect(screen.getByTestId("decision-reset-bar")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "בטל הכרעה ושמור את כל העותקים" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "נבחר לשמירה" })).not.toBeInTheDocument(); + expect(screen.getByText("למחיקה: 0")).toBeInTheDocument(); + }); + + it("lets the user clear the current keep/delete decision for the cluster", () => { + const clearDecision = vi.fn(); + + render( + , + ); + + expect(screen.getByTestId("decision-reset-bar")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "בטל הכרעה ושמור את כל העותקים" })); + + expect(clearDecision).toHaveBeenCalledWith("cluster-1"); + }); + + it("keeps the album comparison area inside a dedicated scroll container", () => { + render( + , + ); + + expect(screen.getByTestId("review-scroll-shell")).toBeInTheDocument(); + expect(screen.getByTestId("comparison-scroller")).toBeInTheDocument(); + expect(screen.getAllByText(/Acoustix/).length).toBeGreaterThan(0); + }); + + it("renders the recommended folder as the first comparison pane", () => { + const reversedCluster = { + ...cluster, + albums: [...cluster.albums].reverse(), + }; + + const { container } = render( + , + ); + + const panes = Array.from(container.querySelectorAll(".ide-pane")); + + expect(panes).toHaveLength(2); + expect(panes[0].textContent).toContain("C:/Music/Acoustix"); + expect(panes[0].textContent).toContain("מומלץ"); + expect(panes[1].textContent).toContain("D:/Archive/Acoustix"); + }); +}); diff --git a/album_deduplicator/frontend/src/components/FinalizeDeletionScreen.jsx b/album_deduplicator/frontend/src/components/FinalizeDeletionScreen.jsx new file mode 100644 index 0000000..bd5b67d --- /dev/null +++ b/album_deduplicator/frontend/src/components/FinalizeDeletionScreen.jsx @@ -0,0 +1,154 @@ +import React from "react"; +import { Button, Popconfirm, Tooltip } from "antd"; +import { Icon, StatusTag } from "./UI"; +import { formatSizeMb, getClusterDisplayTitle } from "../utils"; + +export function FinalizeDeletionScreen({ + workflow, + onExecute, + isExecuting, + openExplorer, + onKeepAllCopies, + feedbackSummary, + onExportFeedback, + onClearFeedbackHistory, +}) { + const { pendingGroups, summary } = workflow; + const feedbackCount = feedbackSummary?.event_count ?? 0; + + return ( +
+
+
+

אישור העברה לסל המחזור ({summary.pendingCount} תיקיות)

+
הפריטים יסומנו לסל המחזור בלבד, ללא מחיקה לצמיתות.
+
+ + + {feedbackCount} אירועי אימון נשמרו + + החלטות ומחיקות בפועל נשמרות מקומית לשיפור המודל. +
+ + + + +
+
+
+
+ onExecute()} + disabled={summary.pendingCount === 0} + > + + +
+
+ +
+ + + + + + + + + + + {pendingGroups.length === 0 ? ( + + ) : ( + pendingGroups.map((group) => { + const score = group.cluster.pairs?.[0] ? `${group.cluster.pairs[0].final_score.toFixed(1)}%` : "N/A"; + + return group.pending.map((item, idx) => ( + + {idx === 0 && ( + <> + + + + + )} + + + )); + }) + )} + +
זיהוי קבוצהאיכות התאמההעותק שיישמר (Keeper)תיקייה מיועדת למחיקה (Trash)
אין פריטים להעברה.
+
+
+
{getClusterDisplayTitle(group.cluster)}
+
{group.cluster.human_summary}
+
+
+ +
+
+
+ {score} + +
+ {group.keeper?.name ?? "ללא Keeper נבחר"} +
{group.keeper?.path ?? "הקבוצה נמצאת בעדכון, אפשר להמתין לרענון."}
+ {group.keeper?.path ? ( + openExplorer(group.keeper.path)}/> + ) : null} +
+
+
+ {item.name} +
{item.path}
+
+ {formatSizeMb(item.estimated_size_mb)} • {item.file_count} קבצים +
+
+
+
+
+ ); +} diff --git a/album_deduplicator/frontend/src/components/ScanPanel.jsx b/album_deduplicator/frontend/src/components/ScanPanel.jsx new file mode 100644 index 0000000..f9cec8a --- /dev/null +++ b/album_deduplicator/frontend/src/components/ScanPanel.jsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import { Button } from "./UI"; + +export function ScanPanel({ + form, + normalizedFolderPaths, + setForm, + onSubmit, + loading, + progress, + summary, + runtimeInfo, + onPickFolders, + onPickPreferredRoot, + onAddFolderRow, + onRemoveFolderRow, + onUpdateFolderPath, +}) { + const [advancedOpen, setAdvancedOpen] = useState(false); + const isComplete = progress.percent >= 100 || progress.stage === "complete"; + + return ( + + ); +} diff --git a/album_deduplicator/frontend/src/components/ScanningScreen.jsx b/album_deduplicator/frontend/src/components/ScanningScreen.jsx new file mode 100644 index 0000000..e6b4724 --- /dev/null +++ b/album_deduplicator/frontend/src/components/ScanningScreen.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Progress, Tag } from "antd"; + +import { getScanProgressModel } from "../scanProgress"; + +export function ScanningScreen({ progress }) { + const { overallPercent, stageLabel, strokeColor, railColor, chipColor, isComplete } = getScanProgressModel(progress); + const current = progress.current ?? 0; + const total = progress.total ?? 0; + + return ( +
+
+
+
+ סריקה פעילה + + {stageLabel} + +
+

סריקה בתהליך

+

מנועי ההשוואה מנתחים את הקבצים

+
+ +
+
+
+
{progress.message || "ממתין"}
+ {overallPercent}% +
+ +
{progress.human_message}
+
+ האחוז הכולל משלב את כל שלבי הניתוח יחד: סריקה, איתור התאמות, חישוב איכות והשוואה. +
+
+
+ פריטים שעובדו + {total > 0 ? `${current}/${total}` : "ממתין לנתונים"} +
+
+ סטטוס + {isComplete ? "הסריקה הושלמה" : stageLabel} +
+
+
+
+
+
+ ); +} diff --git a/album_deduplicator/frontend/src/components/ScoreTransparencyPanel.jsx b/album_deduplicator/frontend/src/components/ScoreTransparencyPanel.jsx new file mode 100644 index 0000000..2161ee1 --- /dev/null +++ b/album_deduplicator/frontend/src/components/ScoreTransparencyPanel.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Popover } from "antd"; +import { Icon } from "./UI"; +import { formatPercent, getRepresentativeClusterPair } from "../utils"; + +export function ScoreTransparencyPanel({ cluster, currentKeeperId }) { + const pair = getRepresentativeClusterPair(cluster, currentKeeperId ?? cluster?.recommended_keeper_id); + if (!pair) return null; + + const content = ( +
+
ציון סופי: {formatPercent(pair.final_score)}
+
אלגוריתם: {formatPercent(pair.algorithmic_score)}
+
ML: {pair.is_identical_by_hash ? "Hash זהה" : formatPercent(pair.ml_score)}
+ {pair.gemini_score &&
AI: {formatPercent(pair.gemini_score)}
} +
+ ); + + return ( + +
+ נתוני השוואה +
+
+ ); +} \ No newline at end of file diff --git a/album_deduplicator/frontend/src/components/SetupScreen.jsx b/album_deduplicator/frontend/src/components/SetupScreen.jsx new file mode 100644 index 0000000..a90ea5d --- /dev/null +++ b/album_deduplicator/frontend/src/components/SetupScreen.jsx @@ -0,0 +1,239 @@ +import React, { useMemo, useState } from "react"; +import { Button, Checkbox, Collapse, Input, Tag } from "antd"; +import { + ArrowDownOutlined, + ArrowUpOutlined, + CloseOutlined, + FolderOpenOutlined, + KeyOutlined, + PlayCircleOutlined, + PlusOutlined, + SettingOutlined, +} from "@ant-design/icons"; + +const ADVANCED_OPTIONS = [ + { + key: "force_rescan", + label: "רענון מלא מהדיסק", + description: "מתעלם מהמידע השמור ומחשב מחדש את כל התיקיות שנבחרו.", + }, + { + key: "full_hash_scan", + label: "בדיקת Hash מלאה", + description: "משפרת דיוק בהשוואה בין עותקים, אך מאריכה את זמן הסריקה.", + }, + { + key: "gemini_enabled", + label: "אימות AI למקרים גבוליים", + description: "מוסיף בדיקת AI רק כשאין הכרעה ברורה בין העותקים.", + }, +]; + +export function SetupScreen({ form, setForm, onSubmit, onPickFolders, onPickFolder, runtimeInfo, geminiSettings }) { + const [showAdvanced, setShowAdvanced] = useState(false); + const handlePathChange = (id, path) => { + setForm((prev) => ({ + ...prev, + folders: prev.folders.map((folder) => (folder.id === id ? { ...folder, path } : folder)), + })); + }; + + const addFolder = () => { + setForm((prev) => ({ ...prev, folders: [...prev.folders, { id: `manual-${Date.now()}`, path: "" }] })); + }; + + const removeFolder = (id) => { + setForm((prev) => ({ ...prev, folders: prev.folders.filter((folder) => folder.id !== id) })); + }; + + const moveFolder = (id, direction) => { + setForm((prev) => { + const currentIndex = prev.folders.findIndex((folder) => folder.id === id); + const nextIndex = currentIndex + direction; + if (currentIndex < 0 || nextIndex < 0 || nextIndex >= prev.folders.length) return prev; + const folders = [...prev.folders]; + [folders[currentIndex], folders[nextIndex]] = [folders[nextIndex], folders[currentIndex]]; + return { ...prev, folders }; + }); + }; + + const hasValidPath = useMemo( + () => form.folders.some((folder) => folder.path.trim().length > 0), + [form.folders], + ); + const runtimeLabel = runtimeInfo?.isElectron ? "אפליקציית Windows" : "גרסת דפדפן"; + const hasSavedGeminiKey = Boolean(geminiSettings?.has_api_key); + + return ( +
+
+
+
+ {runtimeLabel} + סל המחזור בלבד +
+

הגדרת סריקה בטוחה

+

+ מיוזיק אוטומטיק + בחרו תיקיות לבדיקה והמערכת תאתר עבורכם אלבומים כפולים +

+
+ +
+
+ +
+ {form.folders.map((folder, index) => ( +
+ + handlePathChange(folder.id, event.target.value)} + /> + {runtimeInfo?.isElectron ? ( + + ) : null} +
+ ))} +
+
סדר השורות הוא סדר ההעדפה: תיקייה עליונה עדיפה על זו שמתחתיה, גם עבור תתי-תיקיות שנמצאות בתוכה.
+
+ {runtimeInfo?.isElectron ? ( + + ) : null} + +
+
+ +
+ + setForm((prev) => ({ ...prev, use_preferred_roots: event.target.checked }))} + > +
+ הפעל סדר עדיפות לשמירה + כבוי: המערכת תבחר לפי איכות בלבד, בלי להעדיף root מסוים. +
+
+
+ +
+ setShowAdvanced(keys.length > 0)} + items={[ + { + key: "advanced", + label: ( + + + הגדרות מתקדמות + + ), + children: ( +
+ {ADVANCED_OPTIONS.map((option) => ( + setForm((prev) => ({ ...prev, [option.key]: event.target.checked }))} + > +
+ {option.label} + {option.description} +
+
+ ))} +
+
+ + +
+ setForm((prev) => ({ ...prev, gemini_api_key: event.target.value }))} + /> +
+ {hasSavedGeminiKey + ? "מפתח Gemini שמור במחשב הזה. אפשר להשאיר ריק כדי להשתמש בו." + : "המפתח יישמר במחשב הזה לשימוש בסריקות הבאות."} +
+
+
+ ), + }, + ]} + /> +
+
+ +
+ +
+
+
+ ); +} diff --git a/album_deduplicator/frontend/src/components/SummaryScreen.jsx b/album_deduplicator/frontend/src/components/SummaryScreen.jsx new file mode 100644 index 0000000..46d8a0d --- /dev/null +++ b/album_deduplicator/frontend/src/components/SummaryScreen.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Button } from "antd"; +import { Icon } from "./UI"; + +export function SummaryScreen({ summary, onStartReview }) { + const { safe_clusters, review_clusters, compared_pairs } = summary.counts; + + return ( +
+
+
+

הסריקה הושלמה בהצלחה

+

המידע מוכן למעבר

+
+ +
+
+
+ {safe_clusters} + בטוחים למחיקה +
+
+ {review_clusters} + דורשים סקירה +
+
+ {compared_pairs} + זוגות שהושוו +
+
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/album_deduplicator/frontend/src/components/UI.jsx b/album_deduplicator/frontend/src/components/UI.jsx new file mode 100644 index 0000000..aa45d72 --- /dev/null +++ b/album_deduplicator/frontend/src/components/UI.jsx @@ -0,0 +1,34 @@ +import React, { forwardRef } from "react"; +import { + ArrowLeftOutlined, BulbOutlined, CheckCircleOutlined, CheckOutlined, + ClockCircleOutlined, ClusterOutlined, CloseOutlined, CustomerServiceOutlined, + DatabaseOutlined, DeleteOutlined, DownloadOutlined, DownOutlined, ExclamationCircleOutlined, + EyeOutlined, FolderOpenOutlined, InfoCircleOutlined, LineChartOutlined, + PauseOutlined, CaretRightFilled, PlusOutlined, SafetyCertificateOutlined, + SearchOutlined, SettingOutlined, SortAscendingOutlined, StarOutlined, SwapOutlined, UndoOutlined +} from "@ant-design/icons"; + +const iconMap = { + trash: DeleteOutlined, check: CheckOutlined, "check-circle": CheckCircleOutlined, + folder: FolderOpenOutlined, shield: SafetyCertificateOutlined, alert: ExclamationCircleOutlined, + music: CustomerServiceOutlined, eye: EyeOutlined, plus: PlusOutlined, x: CloseOutlined, + settings: SettingOutlined, "chevron-down": DownOutlined, sparkle: StarOutlined, + layers: ClusterOutlined, compare: SwapOutlined, chart: LineChartOutlined, + clock: ClockCircleOutlined, database: DatabaseOutlined, info: InfoCircleOutlined, + "arrow-left": ArrowLeftOutlined, bulb: BulbOutlined, play: CaretRightFilled, pause: PauseOutlined, + scan: SearchOutlined, sort: SortAscendingOutlined, undo: UndoOutlined, download: DownloadOutlined, +}; + +export const Icon = forwardRef(function Icon({ name, size = 16, className = "", style, onClick }, ref) { + const Component = iconMap[name] ?? InfoCircleOutlined; + return