commit 8b8795a475b3f98d990e99e35e05a907b5cb1bf9 Author: Ryan Kegel Date: Wed Sep 9 00:02:16 2026 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/dynamic-deck/.gitignore b/dynamic-deck/.gitignore new file mode 100644 index 0000000..1aca107 --- /dev/null +++ b/dynamic-deck/.gitignore @@ -0,0 +1,8 @@ +# Node.js +node_modules/ + +# Stream Deck files +*.sdPlugin/* +!*.sdPlugin/.appignore +!*.sdPlugin/app*.json +!*.sdPlugin/manifest.json diff --git a/dynamic-deck/.vscode/launch.json b/dynamic-deck/.vscode/launch.json new file mode 100644 index 0000000..1f6423a --- /dev/null +++ b/dynamic-deck/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Plugin", + "type": "node", + "request": "attach", + "processId": "${command:PickProcess}", + "outFiles": [ + "${workspaceFolder}/*.sdPlugin/bin/**/*.js" + ], + "resolveSourceMapLocations": [ + "${workspaceFolder}/**" + ] + } + ] +} \ No newline at end of file diff --git a/dynamic-deck/.vscode/settings.json b/dynamic-deck/.vscode/settings.json new file mode 100644 index 0000000..ef8832f --- /dev/null +++ b/dynamic-deck/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + /* JSON schemas */ + "json.schemas": [ + { + "fileMatch": [ + "**/manifest.json" + ], + "url": "https://schemas.elgato.com/streamdeck/plugins/manifest.json" + }, + { + "fileMatch": [ + "**/layouts/*.json" + ], + "url": "https://schemas.elgato.com/streamdeck/plugins/layout.json" + } + ] +} \ No newline at end of file diff --git a/dynamic-deck/AGENTS.md b/dynamic-deck/AGENTS.md new file mode 100644 index 0000000..3cc8d24 --- /dev/null +++ b/dynamic-deck/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md + +Context for OpenCode sessions working on the **Dynamic Deck** Stream Deck plugin (`com.ryan-kegel.dynamic-deck`). + +## What this is + +An Elgato Stream Deck plugin (SDK v3, Node.js 24) built with TypeScript + Rollup. Some actions are **Windows-only** because they shell out to PowerShell and use Win32 P/Invoke via the `koffi` FFI (`user32.dll`, `kernel32.dll`). Other actions are platform-agnostic. + +## Commands + +- `npm run build` — rollup production build to `com.ryan-kegel.dynamic-deck.sdPlugin/bin/plugin.js` (terser-minified, no sourcemap). +- `npm run watch` — rollup watch with sourcemaps; on every rebuild triggers `streamdeck restart com.ryan-kegel.dynamic-deck`. +- `node generate-assets.cjs` — writes 1x1 placeholder PNGs into `imgs/plugin/` and `imgs/actions/`. Run once after cloning if those images are missing. +- There is **no test runner, linter, or typecheck script**. The only verification is `npm run build` (rollup runs the TS plugin, so type errors will surface here). + +## Source layout + +- `src/plugin.ts` — entrypoint. Registers actions, wires `AppMonitor` (1s polling for foreground-window changes) and `AppTrackerService` (persisted to `app-stats.json` in cwd), handles SIGINT/SIGTERM to flush stats, then `streamDeck.connect()`s. +- `src/actions/.ts` — one `SingletonAction` subclass per Stream Deck action, each `@action({ UUID: "com.ryan-kegel.dynamic-deck." })`. +- `src/services/` — `app-monitor.ts` (Win32 FFI foreground tracking), `app-tracker.ts` (launch stats / ranking), `blacklist-manager.ts`. +- `src/utils/app-launcher.ts` — Start Menu `.lnk` scanning + 256x256 icon extraction via PowerShell + C# `PrivateExtractIcons` P/Invoke. +- `src/ui/.html` + `*.js` — Property Inspector assets, copied as-is to the `.sdPlugin` bundle. +- `com.ryan-kegel.dynamic-deck.sdPlugin/` — the built plugin bundle (output). `manifest.json`, `bin/plugin.js`, copied `ui/`, `imgs/`, `scripts/`, `services/`, `utils/`. This is what Stream Deck loads. + +## Conventions and gotchas + +- **Bundle shape is non-obvious.** `rollup.config.mjs` copies `src/{ui,imgs,scripts,services,utils}` into the `.sdPlugin/` folder alongside the built `bin/plugin.js`. Edits to those files in the source tree won't appear until a rebuild copies them. The TS files copied under `services/` and `utils/` are **not** the ones Rollup bundles — they are inert copies; the bundled code is what ends up in `bin/plugin.js`. +- `RollupWatchPlugin` emits `{"type":"module"}` into `bin/package.json` so the host can `import()` the bundle. Don't hand-edit that file. +- `manifest.json` is treated as a watched file; changes trigger a rebuild. +- Several actions are currently commented out in `plugin.ts` (e.g. `IncrementCounter`, `EcobeeModeAction`, `EcobeeAction`). To re-enable one, uncomment both the import and the `registerAction` line. +- The `app-stats.json` tracker file is written next to `process.cwd()` — i.e. inside the `.sdPlugin/` folder at runtime. Don't delete it without expecting reset ranking data. +- `launch-app.ts` uses `ev.action.setTitle()` to write to Stream Deck's built-in Title field — do **not** add a custom title `` in `launch-app.html`, you'll get two fields (see README §2). +- `node` engine is pinned to v24 via `manifest.json` (`"Debug": "enabled"`); debug by attaching VS Code to the running plugin process (the "Attach to Plugin" launch config in `.vscode/launch.json`). +- `tsconfig.json` extends `@tsconfig/node20` and uses `customConditions: ["node"]` + `moduleResolution: "Bundler"` to match Rollup's resolution mode. + +## Things explicitly NOT to do + +- Don't run `tsc` directly — there's no script and no outDir. The TS plugin runs as part of Rollup. +- Don't add a `test` / `lint` script "just in case" — verify with the user first; none have been configured and the user may not want them. +- Don't assume macOS support exists for app launching features — `app-launcher.ts` and `app-monitor.ts` use Windows-specific APIs. \ No newline at end of file diff --git a/dynamic-deck/README.md b/dynamic-deck/README.md new file mode 100644 index 0000000..65608a3 --- /dev/null +++ b/dynamic-deck/README.md @@ -0,0 +1,123 @@ +# Dynamic Deck - Stream Deck Plugin Architecture & Development Context + +**Plugin ID:** `com.ryan-kegel.dynamic-deck` +**Author:** Ryan Kegel +**Node.js Version:** 24 +**SDK Version:** 3 (Elgato Stream Deck Node.js SDK `@elgato/streamdeck`) + +--- + +## Overview + +`Dynamic Deck` is a custom modular Stream Deck plugin built using TypeScript and Node.js. It integrates desktop application switching, smart home controls, API balance monitoring, and utility actions into a unified plugin structure. + +--- + +## Action Inventory + +### 1. Fast App Launcher (`com.ryan-kegel.dynamic-deck.launch-app`) + +Scans the host system for installed applications, populates a custom dropdown menu in the Property Inspector, automatically extracts application icons, updates button text, and either launches or focuses the selected application. + +- **Property Inspector UI:** Custom HTML/JS interface with styled select elements, dropdown options menu, and options checkboxes. +- **Key Features:** + - **App Discovery:** Scans Windows Start Menu shortcuts (`.lnk` files) via PowerShell scripts and extracts executable paths. + - **High-Resolution Icon Extraction:** Uses Win32 P/Invoke (`PrivateExtractIcons` from `user32.dll`) via C# wrapped inside PowerShell to retrieve 256x256 PNG icons, falling back to standard `ExtractAssociatedIcon`. + - **Title Auto-Generation:** Shortens application executable/shortcut names to a maximum of 2 words (e.g., "Visual Studio Code" -> "Visual Studio") and formats button labels automatically. + - **Process Focus / Bring-to-Front:** Checks running system processes for existing window handles (`MainWindowHandle`). If found, it invokes Win32 `ShowWindow` (`SW_RESTORE = 9`) and `SetForegroundWindow` to bring active windows to the front instead of spawning duplicate app instances. + - **Native UI Title Sync:** Eliminates duplicate input fields in the Property Inspector by leveraging Stream Deck's native built-in Title input and updating it via `ev.action.setTitle()`. + +### 2. DeepSeek Balance (`com.ryan-kegel.dynamic-deck.deepseek-balance`) + +Monitors API key balance for DeepSeek services and opens the developer usage dashboard on key press. + +- **Property Inspector UI:** Modern `sdpi-components` form layout for secure API key configuration. + +### 3. Ecobee Smart Home Controls + +- **Temperature Control (`com.ryan-kegel.dynamic-deck.ecobee`):** Adjusts thermostat target temperature and displays current indoor ambient temperature readings. +- **HVAC Mode (`com.ryan-kegel.dynamic-deck.ecobee-mode`):** Toggles system operating modes (Heat, Cool, Auto, Off). + +### 4. Counter (`com.ryan-kegel.dynamic-deck.increment`) + +A basic numeric state incrementer action. + +--- + +## Key Architectural Decisions & Engineering Solutions + +### 1. High-Resolution Icon Extraction Engine + +Standard Win32 shell calls (`ExtractAssociatedIcon`) extract default `32x32` thumbnail icons, resulting in blurry, pixelated icons on Stream Deck keys. + +**Solution:** In `launch-app.ts`, icon extraction was upgraded by embedding a custom C# helper class (`HighResIconExtractor`) compiled dynamically in PowerShell using `Add-Type`. It requests explicit `256x256` pixel icon groups directly from executable headers: + +```csharp +[DllImport("user32.dll", CharSet = CharSet.Auto)] +private static extern uint PrivateExtractIcons( + string szFileName, int nIconIndex, int cxIcon, int cyIcon, + IntPtr[] phicon, uint[] piconid, uint nIcons, uint flags); +``` + +The high-resolution bitmap is saved directly to an in-memory PNG stream and returned as base64 data to setImage(). + +### 2. Native Property Inspector Title Integration + +Adding a custom `` field to launch-app.html resulted in two Title fields appearing in the settings UI because Stream Deck automatically injects its native Title field at the top of every Property Inspector. + +**Solution:** + +1. The custom HTML row for Title in launch-app.html was removed. +2. When an app is selected in the Property Inspector or picked via OpenFileDialog, the backend (launch-app.ts) calculates the shortened title name and invokes await ev.action.setTitle(title). +3. Stream Deck automatically synchronizes this title to both the physical button face and the native Property Inspector Title input field. + +### 3. Resolving Settings Race Conditions + +Calling `saveSettings()` (`setSettings`) inside the Property Inspector WebSocket script (`launch-app.js`) concurrently with sending message payloads (`sendToPlugin`) created race conditions where stored setting objects were overwritten before backend logic could resolve. + +**Solution:** +Settings management was centralized into the backend action handler (launch-app.ts). The Property Inspector (launch-app.js) sends intent payloads (e.g., { event: "appSelected", app: fullAppObject }) to the backend plugin, which executes icon/title processing and calls await ev.action.setSettings(updatedSettings) in a single atomic transaction. + +### 4. Windows Process Focus Logic + +Standard child_process.execFile calls trigger new executable instances rather than bringing active windows to focus. + +**Solution:** +The plugin passes executable paths to an encoded PowerShell process focus routine before falling back to Start-Process: + +```PowerShell +$proc = Get-Process -Name $exeName -ErrorAction SilentlyContinue \vert{} Where-Object {$_.MainWindowHandle -ne 0 } | Select-Object -First 1; +if ($proc) { + $type::ShowWindow($proc.MainWindowHandle, 9); # SW_RESTORE + $type::SetForegroundWindow($proc.MainWindowHandle); +} else { + Start-Process -FilePath $targetPath; +} +``` + +## Data & Event Flow Sequence + +``` +┌─────────────────────────────────┐ ┌─────────────────────────────────┐ +│ Property Inspector (Client UI) │ │ Plugin Backend (TypeScript) │ +└────────────────┬────────────────┘ └────────────────┬────────────────┘ + │ │ + │────── sendToPlugin: { event: "getApps" } ────────>│ + │ │─── Scans Start Menu + │ │─── Extracts 256x256 Base64 Icons + │<───── sendToPropertyInspector: apps [] ───────────│ + │ │ + │────── sendToPlugin: { event: "appSelected" } ────>│ + │ │─── Formats/Shortens Title + │ │─── ev.action.setTitle(...) + │ │─── ev.action.setImage(...) + │ │─── ev.action.setSettings(...) + │<───── sendToPropertyInspector: { title } ─────────│ +``` + +## File Structure Map + +- manifest.json: Plugin metadata, minimum SDK/OS targets, action declarations, and entry points. +- src/actions/launch-app.ts: Fast App Launcher backend logic (PowerShell app discovery, high-res P/Invoke icon extraction, window activation, settings persistence). +- ui/launch-app.html: Property Inspector structure for App Launcher options and dropdown. +- ui/launch-app.js: Property Inspector logic handling WebSocket initialization, dropdown UI rendering, and message dispatch.ui/deepseek-balance.html: Property Inspector form using for API key entry. diff --git a/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/.appignore b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/.appignore new file mode 100644 index 0000000..c3000c5 --- /dev/null +++ b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/.appignore @@ -0,0 +1,10 @@ +# App Ignore Blacklist +(?i)\\windows\\ +(?i)powershell\.exe$ +(?i)cmd\.exe$ +(?i)explorer\.exe$ +(?i)applicationframehost\.exe$ +(?i)searchhost\.exe$ +(?i)streamdeck\.exe$ +(?i)whatsapp\.root\.exe$ +(?i)steamwebhelper\.exe$ diff --git a/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-names.json b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-names.json new file mode 100644 index 0000000..0a8591d --- /dev/null +++ b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-names.json @@ -0,0 +1,15 @@ +{ + "exact": { + "cmd.exe": "Command Prompt", + "peak.exe": "Peak", + "code.exe": "VS Code", + "chrome.exe": "Google\nChrome", + "PEAK.url": "Peak" + }, + "patterns": [ + { + "regex": "(?i)^godot_v(\\d+)\\..*", + "replacement": "Godot $1" + } + ] +} diff --git a/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-stats.json b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-stats.json new file mode 100644 index 0000000..6bb4c80 --- /dev/null +++ b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/app-stats.json @@ -0,0 +1,154 @@ +{ + "c:\\portableapps\\visual studio code\\code.exe": { + "name": "Visual Studio Code", + "path": "c:\\portableapps\\visual studio code\\code.exe", + "launchCount": 12, + "focusCount": 186, + "activeDurationSeconds": 205450, + "lastUsedTimestamp": 1788926512701 + }, + "c:\\program files\\google\\chrome\\application\\chrome.exe": { + "name": "Google Chrome", + "path": "c:\\program files\\google\\chrome\\application\\chrome.exe", + "launchCount": 34, + "focusCount": 253, + "activeDurationSeconds": 208334, + "lastUsedTimestamp": 1788926520418 + }, + "c:\\portableapps\\blender\\blender.exe": { + "name": "Blender", + "path": "c:\\portableapps\\blender\\blender.exe", + "launchCount": 2, + "focusCount": 2, + "activeDurationSeconds": 6, + "lastUsedTimestamp": 1788410154017 + }, + "c:\\godot4\\godot_v4.7.1-stable_win64.exe": { + "name": "Godot Engine", + "path": "c:\\godot4\\godot_v4.7.1-stable_win64.exe", + "launchCount": 115, + "focusCount": 83, + "activeDurationSeconds": 49481, + "lastUsedTimestamp": 1788919885095 + }, + "c:\\program files\\nomachine\\bin\\nxplayer.bin": { + "name": "NoMachine Player Process", + "path": "c:\\program files\\nomachine\\bin\\nxplayer.bin", + "launchCount": 5, + "focusCount": 26, + "activeDurationSeconds": 3607, + "lastUsedTimestamp": 1788900860130 + }, + "c:\\users\\ryan\\appdata\\local\\programs\\obsidian\\obsidian.exe": { + "name": "Obsidian", + "path": "c:\\users\\ryan\\appdata\\local\\programs\\obsidian\\obsidian.exe", + "launchCount": 3, + "focusCount": 28, + "activeDurationSeconds": 2708, + "lastUsedTimestamp": 1788924951152 + }, + "c:\\program files\\windowsapps\\microsoft.windowsterminal_1.24.11911.0_x64__8wekyb3d8bbwe\\windowsterminal.exe": { + "name": "Windows Terminal Host", + "path": "c:\\program files\\windowsapps\\microsoft.windowsterminal_1.24.11911.0_x64__8wekyb3d8bbwe\\windowsterminal.exe", + "launchCount": 3, + "focusCount": 78, + "activeDurationSeconds": 5848, + "lastUsedTimestamp": 1788925695681 + }, + "h:\\games\\the darkest files\\the darkest files.exe": { + "name": "the darkest files", + "path": "h:\\games\\the darkest files\\the darkest files.exe", + "launchCount": 2, + "focusCount": 4, + "activeDurationSeconds": 2381, + "lastUsedTimestamp": 1788838622190 + }, + "c:\\portableapps\\gimpportable\\app\\gimp\\bin\\gimp-2.10.exe": { + "name": "GNU Image Manipulation Program", + "path": "c:\\portableapps\\gimpportable\\app\\gimp\\bin\\gimp-2.10.exe", + "launchCount": 1, + "focusCount": 1, + "activeDurationSeconds": 3, + "lastUsedTimestamp": 1788575227396 + }, + "c:\\program files\\bambu studio\\bambu-studio.exe": { + "name": "BambuStudio", + "path": "c:\\program files\\bambu studio\\bambu-studio.exe", + "launchCount": 2, + "focusCount": 9, + "activeDurationSeconds": 27027, + "lastUsedTimestamp": 1788923561849 + }, + "c:\\portableapps\\freecad\\bin\\freecad.exe": { + "name": "freecad", + "path": "c:\\portableapps\\freecad\\bin\\freecad.exe", + "launchCount": 3, + "focusCount": 17, + "activeDurationSeconds": 4871, + "lastUsedTimestamp": 1788923536820 + }, + "c:\\portableapps\\irfanview\\app\\irfanview64\\i_view64.exe": { + "name": "IrfanView 64-bit", + "path": "c:\\portableapps\\irfanview\\app\\irfanview64\\i_view64.exe", + "launchCount": 3, + "focusCount": 1, + "activeDurationSeconds": 95, + "lastUsedTimestamp": 1788586171205 + }, + "h:\\steam\\steamapps\\common\\content warning\\content warning.exe": { + "name": "content warning", + "path": "h:\\steam\\steamapps\\common\\content warning\\content warning.exe", + "launchCount": 2, + "focusCount": 10, + "activeDurationSeconds": 3812, + "lastUsedTimestamp": 1788754135308 + }, + "c:\\program files\\7-zip\\7zfm.exe": { + "name": "7-Zip File Manager", + "path": "c:\\program files\\7-zip\\7zfm.exe", + "launchCount": 1, + "focusCount": 3, + "activeDurationSeconds": 35, + "lastUsedTimestamp": 1788751440759 + }, + "c:\\program files\\cheat engine 7.5\\cheatengine-x86_64-sse4-avx2.exe": { + "name": "Cheat Engine", + "path": "c:\\program files\\cheat engine 7.5\\cheatengine-x86_64-sse4-avx2.exe", + "launchCount": 1, + "focusCount": 7, + "activeDurationSeconds": 32, + "lastUsedTimestamp": 1788754937452 + }, + "c:\\program files (x86)\\steam\\steam.exe": { + "name": "Steam", + "path": "c:\\program files (x86)\\steam\\steam.exe", + "launchCount": 1, + "focusCount": 1, + "activeDurationSeconds": 174, + "lastUsedTimestamp": 1788838072451 + }, + "h:\\steam\\steamapps\\common\\peak\\peak.exe": { + "name": "peak", + "path": "h:\\steam\\steamapps\\common\\peak\\peak.exe", + "launchCount": 1, + "focusCount": 1, + "activeDurationSeconds": 22, + "lastUsedTimestamp": 1788838292184 + }, + "d:\\python utilities\\icue\\app\\dist\\icue assigner\\icue assigner.exe": { + "name": "icue assigner", + "path": "d:\\python utilities\\icue\\app\\dist\\icue assigner\\icue assigner.exe", + "launchCount": 1, + "focusCount": 1, + "activeDurationSeconds": 8, + "lastUsedTimestamp": 1788923506351 + }, + "c:\\portableapps\\dimmer\\dimmer.exe": { + "name": "Dimmer - Brightness control software", + "path": "c:\\portableapps\\dimmer\\dimmer.exe", + "launchCount": 1, + "focusCount": 1, + "activeDurationSeconds": 6, + "lastUsedTimestamp": 1788924847522 + } +} \ No newline at end of file diff --git a/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/manifest.json b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/manifest.json new file mode 100644 index 0000000..f311f17 --- /dev/null +++ b/dynamic-deck/com.ryan-kegel.dynamic-deck.sdPlugin/manifest.json @@ -0,0 +1,113 @@ +{ + "Name": "Dynamic Deck", + "Version": "0.1.0.0", + "Author": "Ryan Kegel", + "Actions": [ + { + "Name": "Counter", + "UUID": "com.ryan-kegel.dynamic-deck.increment", + "Icon": "imgs/actions/counter/icon", + "Tooltip": "Displays a count, which increments by one on press.", + "PropertyInspectorPath": "ui/increment-counter.html", + "Controllers": ["Keypad"], + "States": [ + { + "Image": "imgs/actions/counter/key", + "TitleAlignment": "middle" + } + ] + }, + { + "Name": "DeepSeek Balance", + "UUID": "com.ryan-kegel.dynamic-deck.deepseek-balance", + "Icon": "imgs/actions/deepseek/icon", + "Tooltip": "Displays current DeepSeek API balance and opens usage dashboard on press.", + "PropertyInspectorPath": "ui/deepseek-balance.html", + "Controllers": ["Keypad"], + "States": [ + { + "Image": "imgs/actions/deepseek/key", + "TitleAlignment": "middle", + "FontSize": "12" + } + ] + }, + { + "Name": "Ecobee Temperature Control", + "UUID": "com.ryan-kegel.dynamic-deck.ecobee", + "Icon": "imgs/actions/ecobee", + "PropertyInspectorPath": "ui/ecobee.html", + "States": [ + { + "Image": "imgs/actions/key", + "TitleAlignment": "middle", + "FontSize": 10 + } + ] + }, + { + "Name": "Ecobee HVAC Mode", + "UUID": "com.ryan-kegel.dynamic-deck.ecobee-mode", + "Icon": "imgs/actions/ecobee-mode", + "PropertyInspectorPath": "ui/ecobee.html", + "States": [ + { + "Image": "imgs/actions/key", + "TitleAlignment": "middle", + "FontSize": 10 + } + ] + }, + { + "Name": "Fast App Launcher", + "UUID": "com.ryan-kegel.dynamic-deck.launch-app", + "Icon": "imgs/actions/launcher", + "PropertyInspectorPath": "ui/launch-app.html", + "States": [ + { + "Image": "imgs/actions/key", + "TitleAlignment": "middle", + "FontSize": 12 + } + ] + }, + { + "Name": "Dynamic App Slot", + "UUID": "com.ryan-kegel.dynamic-deck.app-slot", + "Icon": "imgs/actions/app-slot", + "PropertyInspectorPath": "ui/app-slot.html", + "States": [ + { + "Image": "imgs/actions/app-slot-state", + "TitleAlignment": "middle", + "FontSize": "12" + } + ], + "PropertyInspectorPath": "ui/app-slot.html" + } + ], + "Category": "Dynamic Deck", + "CategoryIcon": "imgs/plugin/category-icon", + "CodePath": "bin/plugin.js", + "Description": "Just learning", + "Icon": "imgs/plugin/marketplace", + "SDKVersion": 3, + "Software": { + "MinimumVersion": "7.1" + }, + "OS": [ + { + "Platform": "mac", + "MinimumVersion": "12" + }, + { + "Platform": "windows", + "MinimumVersion": "10" + } + ], + "Nodejs": { + "Version": "24", + "Debug": "enabled" + }, + "UUID": "com.ryan-kegel.dynamic-deck" +} diff --git a/dynamic-deck/generate-assets.cjs b/dynamic-deck/generate-assets.cjs new file mode 100644 index 0000000..3569bd9 --- /dev/null +++ b/dynamic-deck/generate-assets.cjs @@ -0,0 +1,30 @@ +// generate-assets.js +const fs = require("fs"); +const path = require("path"); + +// Minimal valid PNG buffer (1x1 solid block) +const minimalPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; +const pngBuffer = Buffer.from(minimalPngBase64, "base64"); + +const assetPaths = [ + "imgs/plugin/category.png", + "imgs/plugin/category@2x.png", + "imgs/actions/app-slot.png", + "imgs/actions/app-slot@2x.png", + "imgs/actions/app-slot-state.png", + "imgs/actions/app-slot-state@2x.png" +]; + +for (const relativePath of assetPaths) { + const fullPath = path.join(process.cwd(), relativePath); + const dir = path.dirname(fullPath); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(fullPath, pngBuffer); + console.log(`Created: ${relativePath}`); +} + +console.log("\nPlaceholder assets created successfully."); \ No newline at end of file diff --git a/dynamic-deck/imgs/actions/app-slot-state.png b/dynamic-deck/imgs/actions/app-slot-state.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/actions/app-slot-state.png differ diff --git a/dynamic-deck/imgs/actions/app-slot-state@2x.png b/dynamic-deck/imgs/actions/app-slot-state@2x.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/actions/app-slot-state@2x.png differ diff --git a/dynamic-deck/imgs/actions/app-slot.png b/dynamic-deck/imgs/actions/app-slot.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/actions/app-slot.png differ diff --git a/dynamic-deck/imgs/actions/app-slot@2x.png b/dynamic-deck/imgs/actions/app-slot@2x.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/actions/app-slot@2x.png differ diff --git a/dynamic-deck/imgs/actions/deepseek/icon.svg b/dynamic-deck/imgs/actions/deepseek/icon.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/dynamic-deck/imgs/actions/deepseek/icon.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/dynamic-deck/imgs/actions/deepseek/key.svg b/dynamic-deck/imgs/actions/deepseek/key.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/dynamic-deck/imgs/actions/deepseek/key.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/dynamic-deck/imgs/plugin/category.png b/dynamic-deck/imgs/plugin/category.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/plugin/category.png differ diff --git a/dynamic-deck/imgs/plugin/category@2x.png b/dynamic-deck/imgs/plugin/category@2x.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dynamic-deck/imgs/plugin/category@2x.png differ diff --git a/dynamic-deck/package-lock.json b/dynamic-deck/package-lock.json new file mode 100644 index 0000000..6d6c2a4 --- /dev/null +++ b/dynamic-deck/package-lock.json @@ -0,0 +1,2806 @@ +{ + "name": "dynamic-deck", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@elgato/streamdeck": "^2.0.0", + "file-icon": "^6.0.0", + "koffi": "^3.1.6" + }, + "devDependencies": { + "@elgato/cli": "^1.9.0", + "@rollup/plugin-commonjs": "^29.0.2", + "@rollup/plugin-node-resolve": "^15.2.2", + "@rollup/plugin-terser": "^1.0.0", + "@rollup/plugin-typescript": "^12.1.0", + "@tsconfig/node20": "^20.1.2", + "@types/node": "~24.1.0", + "rollup": "^4.0.2", + "rollup-plugin-copy": "^3.5.0", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + } + }, + "node_modules/@elgato/cli": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@elgato/cli/-/cli-1.9.0.tgz", + "integrity": "sha512-svzkUhR5+4R2x1fiY05b2s2rEnCs64O9Tg/s/BHByxfimDcjWn1laKT3rDvUrmxgO44p3+w6ItSrk+uijOB05w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@elgato/schemas": "^0.4.14", + "@humanwhocodes/momoa": "^3.0.0", + "@zip.js/zip.js": "^2.7.54", + "ajv": "^8.17.1", + "chalk": "^5.4.1", + "commander": "^13.0.0", + "ejs": "^3.1.10", + "find-process": "^1.4.10", + "ignore": "^7.0.3", + "inquirer": "^12.3.2", + "is-interactive": "^2.0.0", + "lodash": "^4.17.21", + "log-symbols": "^7.0.0", + "rage-edit": "^1.2.0", + "semver": "^7.6.3" + }, + "bin": { + "sd": "bin/streamdeck.mjs", + "streamdeck": "bin/streamdeck.mjs" + }, + "engines": { + "node": ">=20.1.0" + } + }, + "node_modules/@elgato/schemas": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.4.16.tgz", + "integrity": "sha512-PFEi6F36ImO5KCSNHui9o/uXkwjca3hk5b95Df7LgNBdQYREXT3B+P3tYoz6avRuOPlArkKFSkOOvH6RRpjvLQ==", + "license": "MIT" + }, + "node_modules/@elgato/streamdeck": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@elgato/streamdeck/-/streamdeck-2.1.2.tgz", + "integrity": "sha512-qj6NuDyYj8bE+XFnr+O5ynB1zojwugfWiK3oQYwweaOzOFs7fEE21v9z75EPC8wQr3cuuzI7m557TQLn5k/4mA==", + "license": "MIT", + "dependencies": { + "@elgato/schemas": "^0.4.14", + "@elgato/utils": "0.6.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=20.5.1" + } + }, + "node_modules/@elgato/utils": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@elgato/utils/-/utils-0.6.0.tgz", + "integrity": "sha512-DkBf07zo0qzHkoMO2L8HSDlfEwI56rI0fWedJjXcafPpoePoZ/nzqi+xL60Ad2KpQzCxstyN65oqVZ/Ar6woNg==", + "license": "MIT", + "dependencies": { + "zod": "^3.25.24" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.12.tgz", + "integrity": "sha512-xS9xl4ieqTqhSZfKV3YXj/htwJCUxbgvkTVaK1fkESgrOzvoVSOTRQeQ8tTLOZUS0Csi2xqtJQXgVTRH5OEbHQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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/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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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/@koromix/koffi-darwin-arm64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-arm64/-/koffi-darwin-arm64-3.1.6.tgz", + "integrity": "sha512-8FHyXGCZN7/iQf4f7W5BRysmtdlAFvSx6FpmX4u6wmkZiX/2e9hIRdGLiZYlHGudlcA18UmXB/cMiyhJ7fJkzA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-darwin-x64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-x64/-/koffi-darwin-x64-3.1.6.tgz", + "integrity": "sha512-uzx/jqFQuSHqgg1zaRidTBTCfj8Y9M0SDTO8HeoI9s9fJhiJ1mbB9TTwJO5c2hiMnuWg2m1byczC8BaIH6cG/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-arm64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-arm64/-/koffi-freebsd-arm64-3.1.6.tgz", + "integrity": "sha512-PjpTVrsCK5YTtixOw7VsseYXJOyoY6k0qBt+bf0T9h3wyV06y73rALsorFDDEoYpLUBZO7R6EIMs6CpUrEkNTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-ia32": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-ia32/-/koffi-freebsd-ia32-3.1.6.tgz", + "integrity": "sha512-ETYwL820HtFwYoOVzgyvmFmzTHRo9DJtGYTxa5Nb7ajaa5ldCum0jbmUJ3PMECxFTLxD5Q6PYZ8Xbp101bGeSg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-x64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-x64/-/koffi-freebsd-x64-3.1.6.tgz", + "integrity": "sha512-BkqxkNXhAAT9toU2stvLwx1iKHPDx7h08NCICyBbjYEXkCAEr84igTkpE5V3XJ9xZZ2gKll7VvdhxorHtHUqZw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-arm64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-arm64/-/koffi-linux-arm64-3.1.6.tgz", + "integrity": "sha512-cM4XPm9ljbCrcPgXjzFYjDNxDUvvuR7TCYaEoo1AKjwZT/vmWhu2xN3pomfbsHh6aVn80SFA3enufQnaETW1rQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-ia32": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-ia32/-/koffi-linux-ia32-3.1.6.tgz", + "integrity": "sha512-l1SVTpO10iaQt8slbowJpzK4fbwQZ7ufj9tmCyAcIwWUpyAbPS83mJMctU72If6N9/gCS2wuRqwnYB2uPLLhLg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-loong64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-loong64/-/koffi-linux-loong64-3.1.6.tgz", + "integrity": "sha512-KpTJpMSbIdCVFU26ynt0xy4x15h+y6AwPJxj2+iVxJhCzJf4oisCPc0YH2VnutuLV2nVzSrFm7sL/WSOqPgkXw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-riscv64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-riscv64/-/koffi-linux-riscv64-3.1.6.tgz", + "integrity": "sha512-YdFNpsywnXiYOYQlDAatf7TJLnspbGXdmfwIZhf82kbKSflYUsq4tI6NmoUOzM89oIWDGpYqd4Hz3xL7tsyXsw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-x64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-linux-x64/-/koffi-linux-x64-3.1.6.tgz", + "integrity": "sha512-Xx5mpr9VcaMCXfvbqIiLIWIL9Iuu6F4r3iMXg7+zZCqYUFZPFwJgiDQBLxctHv2OYgIfAoaaHMW0GC1cJkHfbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-ia32": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-ia32/-/koffi-openbsd-ia32-3.1.6.tgz", + "integrity": "sha512-39Np4QTxhTlTT6RRveIeP+TnbzrwuDJ0UMyHxEZ+oGtzmb9GqWhl9T1oyehG6v/O+c4BffafG2NwLkCZ7tDKWw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-x64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-x64/-/koffi-openbsd-x64-3.1.6.tgz", + "integrity": "sha512-3EynGn3ycQRqaMWGmUJ0tdtuQdStByqSy/tJ0ZGKWizbMGdFAE73YpgLsyd8BDvwnKWytVG/OLNb5nDHpfd9Dg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-arm64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-win32-arm64/-/koffi-win32-arm64-3.1.6.tgz", + "integrity": "sha512-27FdPPRtT4xbO9bsd2OZa95M5YQ7bcJ8QjCRO57UUMI21REfkDegjqKwqo/CFlugxXlJf5IYtG2rq4BEYIrvxg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-ia32": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-win32-ia32/-/koffi-win32-ia32-3.1.6.tgz", + "integrity": "sha512-5mVelLKVDup4eoxZOpCzCyMPxoctsg+Qe4J9O5BP4KbBEdqoOEqaNEBBRgNzXcdr2g+GGfmIUo+oVR1NvIdiJw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-x64": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@koromix/koffi-win32-x64/-/koffi-win32-x64-3.1.6.tgz", + "integrity": "sha512-lPKjAaHz0aoiZXT/wDVqH+joR5y3lCZj1s9Bk5qx/DGRq+0MK8Ib8VoqiBOrJ69NG5AsJSvn0tQDcSqfRgLmBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz", + "integrity": "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", + "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.10", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.10.tgz", + "integrity": "sha512-5OZgdnxbFuvwm05iVwPXMU5/7iPuVDDZzAnxPDLj+kjN7fwQZSSVgHQhKTNkBPOO6Qef2D5MxPAoPLCGqE2VuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.57", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.57.tgz", + "integrity": "sha512-cd5a0s7CS0MOD52f7FW4ju84wc/+b+YNu5XIG72PfXRLk6rInEdvIrB/BjR8D8hS4fMKuYJOBjv6FoLh3e7DHw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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": "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/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "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/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/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/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/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/file-icon": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-icon/-/file-icon-6.0.0.tgz", + "integrity": "sha512-cNWEJlqKoqcCt8v9ybKL1k69oHqbmEdNSHKUI2o/RmrCiBnG+yJXThfIeljiqfEs/PW1HeazJj2/SJABhtaawQ==", + "license": "MIT", + "dependencies": { + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-process": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz", + "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/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/find-process/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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.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/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-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "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/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "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/koffi": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-3.1.6.tgz", + "integrity": "sha512-ln60chEb3o7Du1ayjwl6BFiNN1wZK+3cTM2wWGiHLEzCY/FdTIN1ER5VWDwHq7J/j4tSnnrHaH5ABS1EO6+6ag==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + }, + "optionalDependencies": { + "@koromix/koffi-darwin-arm64": "3.1.6", + "@koromix/koffi-darwin-x64": "3.1.6", + "@koromix/koffi-freebsd-arm64": "3.1.6", + "@koromix/koffi-freebsd-ia32": "3.1.6", + "@koromix/koffi-freebsd-x64": "3.1.6", + "@koromix/koffi-linux-arm64": "3.1.6", + "@koromix/koffi-linux-ia32": "3.1.6", + "@koromix/koffi-linux-loong64": "3.1.6", + "@koromix/koffi-linux-riscv64": "3.1.6", + "@koromix/koffi-linux-x64": "3.1.6", + "@koromix/koffi-openbsd-ia32": "3.1.6", + "@koromix/koffi-openbsd-x64": "3.1.6", + "@koromix/koffi-win32-arm64": "3.1.6", + "@koromix/koffi-win32-ia32": "3.1.6", + "@koromix/koffi-win32-x64": "3.1.6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/p-map": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.7.tgz", + "integrity": "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/rage-edit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rage-edit/-/rage-edit-1.2.0.tgz", + "integrity": "sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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": { + "queue-microtask": "^1.2.2" + } + }, + "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/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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", + "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "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-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/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/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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "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/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/dynamic-deck/package.json b/dynamic-deck/package.json new file mode 100644 index 0000000..7eb2cc9 --- /dev/null +++ b/dynamic-deck/package.json @@ -0,0 +1,25 @@ +{ + "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w --watch.onEnd=\"streamdeck restart com.ryan-kegel.dynamic-deck\"" + }, + "type": "module", + "devDependencies": { + "@elgato/cli": "^1.9.0", + "@rollup/plugin-commonjs": "^29.0.2", + "@rollup/plugin-node-resolve": "^15.2.2", + "@rollup/plugin-terser": "^1.0.0", + "@rollup/plugin-typescript": "^12.1.0", + "@tsconfig/node20": "^20.1.2", + "@types/node": "~24.1.0", + "rollup": "^4.0.2", + "rollup-plugin-copy": "^3.5.0", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + }, + "dependencies": { + "@elgato/streamdeck": "^2.0.0", + "file-icon": "^6.0.0", + "koffi": "^3.1.6" + } +} diff --git a/dynamic-deck/rollup.config.mjs b/dynamic-deck/rollup.config.mjs new file mode 100644 index 0000000..bd447d1 --- /dev/null +++ b/dynamic-deck/rollup.config.mjs @@ -0,0 +1,65 @@ +import commonjs from "@rollup/plugin-commonjs"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import terser from "@rollup/plugin-terser"; +import copy from "rollup-plugin-copy"; +import typescript from "@rollup/plugin-typescript"; +import path from "node:path"; +import url from "node:url"; + +const isWatching = !!process.env.ROLLUP_WATCH; +const sdPlugin = "com.ryan-kegel.dynamic-deck.sdPlugin"; + +/** + * @type {import('rollup').RollupOptions} + */ +const config = { + input: "src/plugin.ts", + output: { + file: `${sdPlugin}/bin/plugin.js`, + sourcemap: isWatching, + sourcemapPathTransform: (relativeSourcePath, sourcemapPath) => { + return url.pathToFileURL( + path.resolve(path.dirname(sourcemapPath), relativeSourcePath), + ).href; + }, + }, + plugins: [ + copy({ + targets: [ + { src: "src/ui", dest: "com.ryan-kegel.dynamic-deck.sdPlugin" }, + { src: "src/imgs", dest: "com.ryan-kegel.dynamic-deck.sdPlugin" }, + { src: "src/scripts", dest: "com.ryan-kegel.dynamic-deck.sdPlugin" }, + { src: "src/services", dest: "com.ryan-kegel.dynamic-deck.sdPlugin" }, + { src: "src/utils", dest: "com.ryan-kegel.dynamic-deck.sdPlugin" } + ], + }), + { + name: "watch-externals", + buildStart: function () { + this.addWatchFile(`${sdPlugin}/manifest.json`); + }, + }, + typescript({ + mapRoot: isWatching ? "./" : undefined, + }), + nodeResolve({ + browser: false, + exportConditions: ["node"], + preferBuiltins: true, + }), + commonjs(), + !isWatching && terser(), + { + name: "emit-module-package-file", + generateBundle() { + this.emitFile({ + fileName: "package.json", + source: `{ "type": "module" }`, + type: "asset", + }); + }, + }, + ], +}; + +export default config; diff --git a/dynamic-deck/src/actions/app-slot-action.ts b/dynamic-deck/src/actions/app-slot-action.ts new file mode 100644 index 0000000..7c50804 --- /dev/null +++ b/dynamic-deck/src/actions/app-slot-action.ts @@ -0,0 +1,85 @@ +// src/actions/app-slot-action.ts +import streamDeck, { + action, + Action, + DidReceiveSettingsEvent, + KeyDownEvent, + SendToPluginEvent, + SingletonAction, + WillAppearEvent, + WillDisappearEvent +} from "@elgato/streamdeck"; +import { AppLauncher } from "../utils/app-launcher"; +import type { AppTrackerService, TrackedMetric } from "../services/app-tracker"; + +export type AppSlotSettings = { + slotIndex?: number; + metric?: TrackedMetric; + [key: string]: any; +}; + +@action({ UUID: "com.ryan-kegel.dynamic-deck.app-slot" }) +export class AppSlotAction extends SingletonAction { + static appPathMap = new Map(); + static activeActions = new Map>(); + static settingsMap = new Map(); + public static trackerService?: AppTrackerService; + + // Static method called by plugin.ts on app switch + public static refreshAllSlots(): void { + AppSlotAction.trackerService?.queueUpdateSlots(); + } + + // Store visible key instances + private static instances = new Set(); + + override async onWillAppear(ev: WillAppearEvent): Promise { + AppSlotAction.instances.add(this); + AppSlotAction.activeActions.set(ev.action.id, ev.action); + AppSlotAction.settingsMap.set(ev.action.id, ev.payload.settings); + + AppSlotAction.trackerService?.queueUpdateSlots(); + } + + override async onWillDisappear(ev: WillDisappearEvent): Promise { + AppSlotAction.instances.delete(this); + AppSlotAction.activeActions.delete(ev.action.id); + AppSlotAction.settingsMap.delete(ev.action.id); + AppSlotAction.appPathMap.delete(ev.action.id); + + AppSlotAction.trackerService?.clearSlotCache(ev.action.id); + } + + override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise { + AppSlotAction.settingsMap.set(ev.action.id, ev.payload.settings); + AppSlotAction.trackerService?.queueUpdateSlots(); + } + + override async onSendToPlugin(ev: SendToPluginEvent): Promise { + const payload = ev.payload as { command?: string }; + + if (payload?.command === "resetStats") { + streamDeck.logger.info("[AppSlotAction] Received resetStats command from Property Inspector"); + + await AppSlotAction.trackerService?.resetStats(); + AppSlotAction.trackerService?.queueUpdateSlots(); + } + } + + override async onKeyDown(ev: KeyDownEvent): Promise { + const targetPath = AppSlotAction.appPathMap.get(ev.action.id); + + if (!targetPath) { + streamDeck.logger.warn(`[AppSlotAction] Key pressed on unassigned slot (Action ID: ${ev.action.id})`); + await ev.action.showAlert(); + return; + } + + try { + await AppLauncher.activateOrLaunchApp(targetPath, true); + } catch (err) { + streamDeck.logger.error(`[AppSlotAction] Launch failed for path "${targetPath}":`, err); + await ev.action.showAlert(); + } + } +} \ No newline at end of file diff --git a/dynamic-deck/src/actions/deepseek-balance.ts b/dynamic-deck/src/actions/deepseek-balance.ts new file mode 100644 index 0000000..61d444a --- /dev/null +++ b/dynamic-deck/src/actions/deepseek-balance.ts @@ -0,0 +1,140 @@ +import { + action, + KeyDownEvent, + SingletonAction, + WillAppearEvent, + WillDisappearEvent, + DidReceiveSettingsEvent, + streamDeck, +} from "@elgato/streamdeck"; + +type DeepSeekSettings = { + apiKey?: string; +}; + +@action({ UUID: "com.ryan-kegel.dynamic-deck.deepseek-balance" }) +export class DeepSeekBalanceAction extends SingletonAction { + private apiIntervals = new Map(); + private displayIntervals = new Map(); + private cachedBalances = new Map(); + + override async onWillAppear( + ev: WillAppearEvent, + ): Promise { + const actionId = ev.action.id; + + // Fetch initial balance + await this.fetchBalance(ev.action, ev.payload.settings); + + // Poll API every 15 minutes + const apiInterval = setInterval( + () => { + this.fetchBalance(ev.action, ev.payload.settings); + }, + 15 * 60 * 1000, + ); + this.apiIntervals.set(actionId, apiInterval); + + // Refresh display every 1 minute (to update Peak/Off-Peak indicator accurately) + const displayInterval = setInterval(() => { + this.updateDisplay(ev.action); + }, 60 * 1000); + this.displayIntervals.set(actionId, displayInterval); + } + + override async onWillDisappear( + ev: WillDisappearEvent, + ): Promise { + const actionId = ev.action.id; + + if (this.apiIntervals.has(actionId)) { + clearInterval(this.apiIntervals.get(actionId)!); + this.apiIntervals.delete(actionId); + } + if (this.displayIntervals.has(actionId)) { + clearInterval(this.displayIntervals.get(actionId)!); + this.displayIntervals.delete(actionId); + } + } + + override async onDidReceiveSettings( + ev: DidReceiveSettingsEvent, + ): Promise { + await this.fetchBalance(ev.action, ev.payload.settings); + } + + override async onKeyDown(ev: KeyDownEvent): Promise { + await streamDeck.system.openUrl("https://platform.deepseek.com/usage"); + } + + /** + * Checks if current UTC time falls within DeepSeek Peak hours: + * Peak = Monday-Friday, 01:00-04:00 UTC or 06:00-10:00 UTC + */ + private isPeakTime(): boolean { + const now = new Date(); + const day = now.getUTCDay(); // 0 = Sunday, 1 = Monday, ..., 5 = Friday, 6 = Saturday + + if (day >= 1 && day <= 5) { + const hour = now.getUTCHours(); + if ((hour >= 1 && hour < 4) || (hour >= 6 && hour < 10)) { + return true; + } + } + return false; + } + + private updateDisplay(action: any): void { + const balance = this.cachedBalances.get(action.id); + if (!balance) return; + + const isPeak = this.isPeakTime(); + const statusBadge = isPeak ? "🔴" : "🟢"; + + // Display format: + // $12.34 + // 🟢 OFF or 🔴 PEAK + action.setTitle(`DeepSeek\n${statusBadge}${balance}`); + } + + private async fetchBalance( + action: any, + settings: DeepSeekSettings, + ): Promise { + if (!settings.apiKey) { + await action.setTitle("Set API\nKey"); + return; + } + + try { + const res = await fetch("https://api.deepseek.com/user/balance", { + headers: { + Authorization: `Bearer ${settings.apiKey}`, + }, + }); + + if (!res.ok) { + await action.setTitle("Error\n" + res.status); + return; + } + + const data = (await res.json()) as { + balance_infos?: Array<{ currency: string; total_balance: string }>; + }; + + const info = data.balance_infos?.[0]; + if (info) { + const symbol = info.currency === "USD" ? "$" : "¥"; + const amount = parseFloat(info.total_balance).toFixed(2); + + // Cache current formatted balance string + this.cachedBalances.set(action.id, `${symbol}${amount}`); + this.updateDisplay(action); + } else { + await action.setTitle("No Data"); + } + } catch { + await action.setTitle("Conn\nErr"); + } + } +} diff --git a/dynamic-deck/src/actions/ecobee-mode.ts b/dynamic-deck/src/actions/ecobee-mode.ts new file mode 100644 index 0000000..ab3583c --- /dev/null +++ b/dynamic-deck/src/actions/ecobee-mode.ts @@ -0,0 +1,202 @@ +import streamDeck, { + action, + KeyDownEvent, + SingletonAction, + WillAppearEvent, + WillDisappearEvent, + DidReceiveSettingsEvent, + KeyAction, + DialAction +} from "@elgato/streamdeck"; + +type EcobeeModeSettings = { + haUrl?: string; + accessToken?: string; + entityId?: string; + hvacMode?: string; +}; + +type EcobeeModeActionInstance = KeyAction | DialAction; + +const MODE_ORDER = ["off", "heat", "cool", "heat_cool"]; + +@action({ UUID: "com.ryan-kegel.dynamic-deck.ecobee-mode" }) +export class EcobeeModeAction extends SingletonAction { + private pollIntervals = new Map(); + private activeActions = new Map(); + + private async getResolvedSettings(local: EcobeeModeSettings): Promise { + const global = await streamDeck.settings.getGlobalSettings(); + return { + ...local, + haUrl: local.haUrl || global.haUrl, + accessToken: local.accessToken || global.accessToken, + entityId: local.entityId || global.entityId + }; + } + + override async onWillAppear(ev: WillAppearEvent): Promise { + const actionId = ev.action.id; + this.activeActions.set(actionId, ev.action); + + const s = await this.getResolvedSettings(ev.payload.settings); + await this.fetchStatus(ev.action, s); + + const interval = setInterval(async () => { + const currentSettings = await this.getResolvedSettings(await ev.action.getSettings()); + this.fetchStatus(ev.action, currentSettings); + }, 60 * 1000); + + this.pollIntervals.set(actionId, interval); + } + + override async onWillDisappear(ev: WillDisappearEvent): Promise { + const actionId = ev.action.id; + this.activeActions.delete(actionId); + + const interval = this.pollIntervals.get(actionId); + if (interval) { + clearInterval(interval); + this.pollIntervals.delete(actionId); + } + } + + override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise { + const s = await this.getResolvedSettings(ev.payload.settings); + await this.fetchStatus(ev.action, s); + } + +override async onKeyDown(ev: KeyDownEvent): Promise { + const s = await this.getResolvedSettings(ev.payload.settings); + + if (!s.haUrl || !s.accessToken || !s.entityId) { + await ev.action.showAlert(); + return; + } + + try { + const baseUrl = s.haUrl.replace(/\/$/, ""); + let targetMode = s.hvacMode || "cycle"; + + if (targetMode === "cycle") { + const stateRes = await fetch(`${baseUrl}/api/states/${s.entityId}`, { + headers: { + "Authorization": `Bearer ${s.accessToken}`, + "Content-Type": "application/json" + } + }); + + if (!stateRes.ok) { + await ev.action.showAlert(); + return; + } + + const stateData = (await stateRes.json()) as any; + const currentMode = stateData.state; + const currentIndex = MODE_ORDER.indexOf(currentMode); + const nextIndex = (currentIndex + 1) % MODE_ORDER.length; + targetMode = MODE_ORDER[nextIndex]; + } + + const res = await fetch(`${baseUrl}/api/services/climate/set_hvac_mode`, { + method: "POST", + headers: { + "Authorization": `Bearer ${s.accessToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + entity_id: s.entityId, + hvac_mode: targetMode + }) + }); + + if (res.ok) { + // Immediately broadcast & re-render SVG without showing the green checkmark overlay + await this.broadcastStatusUpdate(); + } else { + await ev.action.showAlert(); + } + } catch { + await ev.action.showAlert(); + } +} + private async broadcastStatusUpdate(): Promise { + for (const action of this.activeActions.values()) { + const local = await action.getSettings(); + const resolved = await this.getResolvedSettings(local); + await this.fetchStatus(action, resolved); + } + } + + private async fetchStatus(action: EcobeeModeActionInstance, settings: EcobeeModeSettings): Promise { + if (!settings.haUrl || !settings.accessToken || !settings.entityId) { + await action.setTitle("HA\nNo Token"); + return; + } + + try { + const baseUrl = settings.haUrl.replace(/\/$/, ""); + const res = await fetch(`${baseUrl}/api/states/${settings.entityId}`, { + headers: { + "Authorization": `Bearer ${settings.accessToken}`, + "Content-Type": "application/json" + } + }); + + if (!res.ok) { + await action.setTitle("HA\nErr"); + return; + } + + const data = (await res.json()) as any; + const currentMode = data.state; + const hvacAction = data.attributes.hvac_action || currentMode; + const currentTemp = Math.round(data.attributes.current_temperature); + + await action.setTitle(""); + const svgDataUri = this.generateModeCardSvg(currentMode, hvacAction, currentTemp, settings.hvacMode); + await action.setImage(svgDataUri); + } catch { + await action.setTitle("Conn\nErr"); + } + } + + private generateModeCardSvg(currentMode: string, hvacAction: string, currentTemp: number, configuredMode?: string): string { + let headerText = "MODE"; + let headerColor = "#2c3e50"; + + const activeType = configuredMode && configuredMode !== "cycle" ? configuredMode : currentMode; + + switch (activeType) { + case "cool": + headerText = "❄ COOL"; + headerColor = "#2980b9"; + break; + case "heat": + headerText = "🔥 HEAT"; + headerColor = "#e74c3c"; + break; + case "heat_cool": + headerText = "🔄 AUTO"; + headerColor = "#8e44ad"; + break; + case "off": + headerText = "⭕ OFF"; + headerColor = "#7f8c8d"; + break; + } + + const actionLabel = hvacAction.toUpperCase(); + + const svg = ` + + + + ${headerText} + ${actionLabel} + ROOM: ${currentTemp}° + `.trim(); + + return `data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}`; + } +} \ No newline at end of file diff --git a/dynamic-deck/src/actions/ecobee.ts b/dynamic-deck/src/actions/ecobee.ts new file mode 100644 index 0000000..20c3eeb --- /dev/null +++ b/dynamic-deck/src/actions/ecobee.ts @@ -0,0 +1,245 @@ +import streamDeck, { + action, + KeyDownEvent, + SingletonAction, + WillAppearEvent, + WillDisappearEvent, + DidReceiveSettingsEvent, + KeyAction, + DialAction +} from "@elgato/streamdeck"; + +type EcobeeSettings = { + haUrl?: string; + accessToken?: string; + entityId?: string; + step?: string; + targetTemp?: string; + targetCoolTemp?: string; + targetHeatTemp?: string; +}; + +type EcobeeActionInstance = KeyAction | DialAction; + +@action({ UUID: "com.ryan-kegel.dynamic-deck.ecobee" }) +export class EcobeeAction extends SingletonAction { + private pollIntervals = new Map(); + private activeActions = new Map(); + + private async getResolvedSettings(local: EcobeeSettings): Promise { + const global = await streamDeck.settings.getGlobalSettings(); + return { + ...local, + haUrl: local.haUrl || global.haUrl, + accessToken: local.accessToken || global.accessToken, + entityId: local.entityId || global.entityId + }; + } + + override async onWillAppear(ev: WillAppearEvent): Promise { + const actionId = ev.action.id; + this.activeActions.set(actionId, ev.action); + + const s = await this.getResolvedSettings(ev.payload.settings); + await this.fetchStatus(ev.action, s); + + const interval = setInterval(async () => { + const currentSettings = await this.getResolvedSettings(await ev.action.getSettings()); + this.fetchStatus(ev.action, currentSettings); + }, 60 * 1000); + + this.pollIntervals.set(actionId, interval); + } + + override async onWillDisappear(ev: WillDisappearEvent): Promise { + const actionId = ev.action.id; + this.activeActions.delete(actionId); + + const interval = this.pollIntervals.get(actionId); + if (interval) { + clearInterval(interval); + this.pollIntervals.delete(actionId); + } + } + + override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise { + const s = await this.getResolvedSettings(ev.payload.settings); + await this.fetchStatus(ev.action, s); + } + + override async onKeyDown(ev: KeyDownEvent): Promise { + const s = await this.getResolvedSettings(ev.payload.settings); + + if (!s.haUrl || !s.accessToken || !s.entityId) { + await ev.action.showAlert(); + return; + } + + try { + const baseUrl = s.haUrl.replace(/\/$/, ""); + + // --- Stepper Mode (+1 / -1) --- + if (s.step) { + const stepVal = parseFloat(s.step); + if (isNaN(stepVal)) { + await ev.action.showAlert(); + return; + } + + const stateRes = await fetch(`${baseUrl}/api/states/${s.entityId}`, { + headers: { + "Authorization": `Bearer ${s.accessToken}`, + "Content-Type": "application/json" + } + }); + + if (!stateRes.ok) { + await ev.action.showAlert(); + return; + } + + const stateData = (await stateRes.json()) as any; + let activeTarget = + stateData.attributes.temperature ?? + stateData.attributes.target_temp_high ?? + stateData.attributes.current_temperature ?? + 72; + + activeTarget += stepVal; + + const res = await fetch(`${baseUrl}/api/services/climate/set_temperature`, { + method: "POST", + headers: { + "Authorization": `Bearer ${s.accessToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + entity_id: s.entityId, + temperature: activeTarget + }) + }); + + if (res.ok) { + await this.broadcastStatusUpdate(); + } else { + await ev.action.showAlert(); + } + return; + } + + // --- Static / Dual Setpoint Fallback --- + const payload: Record = { entity_id: s.entityId }; + + const temp = s.targetTemp ? parseFloat(s.targetTemp) : null; + const cool = s.targetCoolTemp ? parseFloat(s.targetCoolTemp) : null; + const heat = s.targetHeatTemp ? parseFloat(s.targetHeatTemp) : null; + + if (temp !== null) { + payload.temperature = temp; + } else if (cool !== null && heat !== null) { + payload.target_temp_high = cool; + payload.target_temp_low = heat; + } else if (cool !== null) { + payload.temperature = cool; + payload.target_temp_high = cool; + } else if (heat !== null) { + payload.temperature = heat; + payload.target_temp_low = heat; + } else { + await ev.action.showAlert(); + return; + } + + const res = await fetch(`${baseUrl}/api/services/climate/set_temperature`, { + method: "POST", + headers: { + "Authorization": `Bearer ${s.accessToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + await this.broadcastStatusUpdate(); + } else { + await ev.action.showAlert(); + } + } catch { + await ev.action.showAlert(); + } + } + + private async broadcastStatusUpdate(): Promise { + for (const action of this.activeActions.values()) { + const local = await action.getSettings(); + const resolved = await this.getResolvedSettings(local); + await this.fetchStatus(action, resolved); + } + } + + private async fetchStatus(action: EcobeeActionInstance, settings: EcobeeSettings): Promise { + if (!settings.haUrl || !settings.accessToken || !settings.entityId) { + await action.setTitle("HA\nNo Token"); + return; + } + + try { + const baseUrl = settings.haUrl.replace(/\/$/, ""); + const res = await fetch(`${baseUrl}/api/states/${settings.entityId}`, { + headers: { + "Authorization": `Bearer ${settings.accessToken}`, + "Content-Type": "application/json" + } + }); + + if (!res.ok) { + await action.setTitle("HA\nErr"); + return; + } + + const data = (await res.json()) as any; + const currentTemp = Math.round(data.attributes.current_temperature); + + const setTemp = + data.attributes.temperature ?? + (data.attributes.target_temp_high + ? `${data.attributes.target_temp_low}-${data.attributes.target_temp_high}` + : null) ?? + settings.targetTemp ?? + "--"; + + await action.setTitle(""); + const svgDataUri = this.generateCardSvg(currentTemp, setTemp.toString(), settings.step); + await action.setImage(svgDataUri); + } catch { + await action.setTitle("Conn\nErr"); + } + } + + private generateCardSvg(currentTemp: number, setTemp: string, step?: string): string { + let headerText = "ECOBEE"; + let headerColor = "#2c3e50"; + + if (step) { + const num = parseFloat(step); + if (num > 0) { + headerText = `▲ +${num}° UP`; + headerColor = "#e74c3c"; + } else if (num < 0) { + headerText = `▼ ${num}° DOWN`; + headerColor = "#2980b9"; + } + } + + const svg = ` + + + + ${headerText} + ${currentTemp}° + SET: ${setTemp}° + `.trim(); + + return `data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}`; + } +} \ No newline at end of file diff --git a/dynamic-deck/src/actions/increment-counter.ts b/dynamic-deck/src/actions/increment-counter.ts new file mode 100644 index 0000000..263d339 --- /dev/null +++ b/dynamic-deck/src/actions/increment-counter.ts @@ -0,0 +1,49 @@ +import { + action, + KeyDownEvent, + SingletonAction, + WillAppearEvent, +} from "@elgato/streamdeck"; + +/** + * An example action class that displays a count that increments by one each time the button is pressed. + */ +@action({ UUID: "com.ryan-kegel.dynamic-deck.increment" }) +export class IncrementCounter extends SingletonAction { + /** + * The {@link SingletonAction.onWillAppear} event is useful for setting the visual representation of an action when it becomes visible. This could be due to the Stream Deck first + * starting up, or the user navigating between pages / folders etc.. There is also an inverse of this event in the form of {@link streamDeck.client.onWillDisappear}. In this example, + * we're setting the title to the "count" that is incremented in {@link IncrementCounter.onKeyDown}. + */ + override onWillAppear( + ev: WillAppearEvent, + ): void | Promise { + return ev.action.setTitle(`${ev.payload.settings.count ?? 0}`); + } + + /** + * Listens for the {@link SingletonAction.onKeyDown} event which is emitted by Stream Deck when an action is pressed. Stream Deck provides various events for tracking interaction + * with devices including key down/up, dial rotations, and device connectivity, etc. When triggered, {@link ev} object contains information about the event including any payloads + * and action information where applicable. In this example, our action will display a counter that increments by one each press. We track the current count on the action's persisted + * settings using `setSettings` and `getSettings`. + */ + override async onKeyDown(ev: KeyDownEvent): Promise { + // Update the count from the settings. + const { settings } = ev.payload; + settings.incrementBy ??= 1; + settings.count = (settings.count ?? 0) + settings.incrementBy; + + // Update the current count in the action's settings, and change the title. + await ev.action.setSettings(settings); + await ev.action.setTitle(`${settings.count}`); + await ev.action.showAlert(); + } +} + +/** + * Settings for {@link IncrementCounter}. + */ +type CounterSettings = { + count?: number; + incrementBy?: number; +}; diff --git a/dynamic-deck/src/actions/launch-app.ts b/dynamic-deck/src/actions/launch-app.ts new file mode 100644 index 0000000..1fabeee --- /dev/null +++ b/dynamic-deck/src/actions/launch-app.ts @@ -0,0 +1,143 @@ +// src/actions/launch-app-action.ts +import streamDeck, { + action, + DidReceiveSettingsEvent, + KeyDownEvent, + SendToPluginEvent, + SingletonAction, + WillAppearEvent +} from "@elgato/streamdeck"; +import { AppTrackerService } from "../services/app-tracker"; +import { AppLauncher, InstalledApp } from "../utils/app-launcher"; + +const logger = streamDeck.logger.createScope("AppLauncherButton"); + +export interface LaunchAppSettings { + appPreset?: string; + customApp?: InstalledApp; + bringToFront?: boolean; + title?: string; + [key: string]: any; +} + +@action({ UUID: "com.ryan-kegel.dynamic-deck.launch-app" }) +export class LaunchAppAction extends SingletonAction { + public static trackerService?: AppTrackerService; + + override async onWillAppear(ev: WillAppearEvent): Promise { + await this.updateButtonVisuals(ev.action, ev.payload.settings); + } + + override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise { + await this.updateButtonVisuals(ev.action, ev.payload.settings); + } + + override async onSendToPlugin(ev: SendToPluginEvent, LaunchAppSettings>): Promise { + if (ev.payload.event === "getApps") { + try { + const apps = await AppLauncher.getInstalledApps(); + await streamDeck.ui.sendToPropertyInspector({ + event: "getAppsResponse", + apps: apps + }); + } catch { + await streamDeck.ui.sendToPropertyInspector({ + event: "getAppsResponse", + apps: [] + }); + } + } else if (ev.payload.event === "pickFile") { + const result = await AppLauncher.openExecutablePicker(); + if (result) { + const title = AppLauncher.shortenTitle(result.name); + + const updatedSettings: LaunchAppSettings = { + ...ev.payload.settings, + appPreset: result.path, + customApp: result, + title: title + }; + await ev.action.setSettings(updatedSettings); + await this.updateButtonVisuals(ev.action, updatedSettings); + + await streamDeck.ui.sendToPropertyInspector({ + event: "filePickedResponse", + app: result, + title: title + }); + } + } else if (ev.payload.event === "appSelected" && ev.payload.app) { + const app = ev.payload.app as InstalledApp; + const title = AppLauncher.shortenTitle(app.name); + + const updatedSettings: LaunchAppSettings = { + ...ev.payload.settings, + appPreset: app.path, + customApp: app, + title: title + }; + await ev.action.setSettings(updatedSettings); + await this.updateButtonVisuals(ev.action, updatedSettings); + + await streamDeck.ui.sendToPropertyInspector({ + event: "appSelectedResponse", + title: title + }); + } + } + + override async onKeyDown(ev: KeyDownEvent): Promise { + const targetPath = ev.payload.settings.appPreset; + const bringToFront = ev.payload.settings.bringToFront !== false; + + if (!targetPath || targetPath === "other") { + await ev.action.showAlert(); + return; + } + + try { + logger.info(`[LaunchAppAction] Activating or launching app at path "${targetPath}"`); + await AppLauncher.activateOrLaunchApp(targetPath, bringToFront); + } catch { + await ev.action.showAlert(); + } + } + + private async updateButtonVisuals(actionContext: any, settings: LaunchAppSettings): Promise { + const targetPath = settings.customApp?.path || settings.appPreset; + + if (!targetPath || targetPath === "other") { + await actionContext.setImage(null); + await actionContext.setTitle(""); + return; + } + + // Restore Title + let titleToSet = settings.title; + if (!titleToSet && settings.customApp?.name) { + titleToSet = AppLauncher.shortenTitle(settings.customApp.name); + } + + if (titleToSet !== undefined) { + await actionContext.setTitle(titleToSet); + } + + // Restore High-Res Image + try { + const highResIcon = await AppLauncher.getAppIcon(targetPath); + + if (highResIcon) { + await actionContext.setImage(this.formatIconUri(highResIcon)); + } else { + await actionContext.setImage(null); + } + } catch (err) { + logger.error(`[LaunchAppAction] Failed to update button icon for "${targetPath}":`, err); + await actionContext.setImage(null); + } + } + + private formatIconUri(icon: string): string { + return icon.startsWith("data:") ? icon : `data:image/png;base64,${icon}`; + } +} \ No newline at end of file diff --git a/dynamic-deck/src/plugin.ts b/dynamic-deck/src/plugin.ts new file mode 100644 index 0000000..034f976 --- /dev/null +++ b/dynamic-deck/src/plugin.ts @@ -0,0 +1,64 @@ +import streamDeck from "@elgato/streamdeck"; +import path from "path"; +import { AppSlotAction } from "./actions/app-slot-action"; +import { DeepSeekBalanceAction } from "./actions/deepseek-balance"; +import { EcobeeAction } from "./actions/ecobee"; +import { EcobeeModeAction } from "./actions/ecobee-mode"; +import { LaunchAppAction } from "./actions/launch-app"; +import { AppMonitor, type AppSwitchEvent } from "./services/app-monitor"; +import { AppTrackerService } from "./services/app-tracker"; + +const logger = streamDeck.logger.createScope("Plugin"); + +const statsPath = path.join(process.cwd(), "app-stats.json"); + +const tracker = new AppTrackerService(statsPath); +AppSlotAction.trackerService = tracker; +LaunchAppAction.trackerService = tracker; + + +// Register all Stream Deck actions +streamDeck.actions.registerAction(new DeepSeekBalanceAction()); +streamDeck.actions.registerAction(new EcobeeModeAction()); +streamDeck.actions.registerAction(new EcobeeAction()); +streamDeck.actions.registerAction(new LaunchAppAction()); +streamDeck.actions.registerAction(new AppSlotAction()); + +// Initialize monitor with a 1000ms polling interval +const monitor = new AppMonitor(1000); + +// Handle app switch events + +monitor.on("appSwitched", (event: AppSwitchEvent) => { + const normalizedPath = path.normalize(event.path).toLowerCase(); + const normalizedEvent: AppSwitchEvent = { ...event, path: normalizedPath }; + + const appName = path.basename(normalizedPath); + logger.info(`App Focused: ${appName}`); + + tracker.handleAppSwitch(normalizedEvent); + AppSlotAction.refreshAllSlots(); + + const topByLaunch = tracker.getRankedApps("launchCount").slice(0, 3); + logger.info("Top 3 by Launch Count: " + topByLaunch.map((a) => `${a.name} (${a.launchCount})`).join(", ")); +}); + +monitor.start(); + +// Save stats gracefully on process exit + +const saveAndExit = () => { + logger.info("Cleaning up and saving tracker stats before exit..."); + try { + tracker.destroy(); + } catch (err) { + logger.error("Error saving stats on exit:", err); + } + process.exit(0); +}; + +process.on("SIGINT", saveAndExit); +process.on("SIGTERM", saveAndExit); + +// Connect to Stream Deck +streamDeck.connect(); \ No newline at end of file diff --git a/dynamic-deck/src/services/app-monitor.ts b/dynamic-deck/src/services/app-monitor.ts new file mode 100644 index 0000000..18b27c5 --- /dev/null +++ b/dynamic-deck/src/services/app-monitor.ts @@ -0,0 +1,110 @@ +// src/services/app-monitor.ts +import EventEmitter from "events"; +import koffi from "koffi"; +import streamDeck from "@elgato/streamdeck"; + +const logger = streamDeck.logger.createScope("AppMonitor"); + +// Load Windows API DLLs +const user32 = koffi.load("user32.dll"); +const kernel32 = koffi.load("kernel32.dll"); + +// Define Win32 Functions +const GetForegroundWindow = user32.func("void* GetForegroundWindow()"); +const GetWindowThreadProcessId = user32.func("uint32 GetWindowThreadProcessId(void* hWnd, _Out_ uint32* lpdwProcessId)"); +const OpenProcess = kernel32.func("void* OpenProcess(uint32 dwDesiredAccess, int bInheritHandle, uint32 dwProcessId)"); +const QueryFullProcessImageNameW = kernel32.func("int QueryFullProcessImageNameW(void* hProcess, uint32 dwFlags, _Out_ uint16* lpExeName, _Inout_ uint32* lpdwSize)"); +const CloseHandle = kernel32.func("int CloseHandle(void* hObject)"); + +const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000; + +export interface AppSwitchEvent { + path: string; + pid?: number; + timestamp: number; +} + +export interface ActiveWindowInfo { + path: string; + pid: number; +} + +export class AppMonitor extends EventEmitter { + private monitorTimer: NodeJS.Timeout | null = null; + private lastActivePath: string | null = null; + private lastActivePid: number | null = null; + + constructor(private intervalMs: number = 1000) { + super(); + } + + public start(): void { + if (this.monitorTimer) return; + logger.info(`Starting native AppMonitor with ${this.intervalMs}ms polling interval.`); + this.monitorTimer = setInterval(() => this.checkActiveWindow(), this.intervalMs); + } + + public stop(): void { + if (this.monitorTimer) { + clearInterval(this.monitorTimer); + this.monitorTimer = null; + } + } + +private checkActiveWindow(): void { + const activeWindow = this.getActiveWindowInfo(); + if (!activeWindow) return; + + // Skip if both path and PID have not changed + if (activeWindow.path === this.lastActivePath && activeWindow.pid === this.lastActivePid) { + return; + } + + this.lastActivePath = activeWindow.path; + this.lastActivePid = activeWindow.pid; + + const event: AppSwitchEvent = { + path: activeWindow.path, + pid: activeWindow.pid, + timestamp: Date.now() + }; + logger.info(`App Focused: ${event.path} (PID: ${event.pid})`); + + this.emit("appSwitched", event); +} + +private getActiveWindowInfo(): ActiveWindowInfo | null { + const hwnd = GetForegroundWindow(); + if (!hwnd) return null; + + const pidBuffer = [0]; + GetWindowThreadProcessId(hwnd, pidBuffer); + const pid = pidBuffer[0]; + if (!pid) return null; + + const hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); + if (!hProcess) return null; + + try { + const bufSize = 1024; + const buffer = new Uint16Array(bufSize); + const sizeRef = [bufSize]; + + const success = QueryFullProcessImageNameW(hProcess, 0, buffer, sizeRef); + if (success) { + const charCount = sizeRef[0]; + const rawPath = String.fromCharCode(...buffer.slice(0, charCount)); + const normalizedPath = rawPath.toLowerCase().replace(/\//g, "\\"); + + return { + path: normalizedPath, + pid: pid + }; + } + } finally { + CloseHandle(hProcess); + } + + return null; +} +} \ No newline at end of file diff --git a/dynamic-deck/src/services/app-tracker.ts b/dynamic-deck/src/services/app-tracker.ts new file mode 100644 index 0000000..e2d3820 --- /dev/null +++ b/dynamic-deck/src/services/app-tracker.ts @@ -0,0 +1,385 @@ +import streamDeck from "@elgato/streamdeck"; +import * as fs from "fs"; +import * as path from "path"; +import { AppSlotAction } from "../actions/app-slot-action"; +import { AppLauncher } from "../utils/app-launcher"; +import { AppSwitchEvent } from "./app-monitor"; +import { AppMetadata } from "../utils/app-metadata"; +import { AppNameMapper } from "../utils/app-name-mapper"; + +const logger = streamDeck.logger.createScope("AppTrackerService"); + +export interface AppStat { + name: string; + path: string; + launchCount: number; + focusCount: number; + activeDurationSeconds: number; + lastUsedTimestamp: number; +} + +export type TrackedMetric = "launchCount" | "focusCount" | "lastUsed" | "activeDuration"; + +interface RenderedSlotState { + appPath: string; + title: string; +} + +export class AppTrackerService { + private nameMapper: AppNameMapper = new AppNameMapper(); + private stats: Map = new Map(); + private activePath: string | null = null; + private activeStartTime: number = Date.now(); + + private knownPids: Set = new Set(); + private readonly MAX_TRACKED_PIDS = 500; + + private ignorePatterns: RegExp[] = []; + + private renderedSlots: Map = new Map(); + + // Timers + private durationTimer: NodeJS.Timeout | null = null; + private updateTimer: NodeJS.Timeout | null = null; + private saveTimer: NodeJS.Timeout | null = null; + + constructor( + private storageFilePath: string, + ignoreFilePath: string = path.join(process.cwd(), ".appignore") + ) { + this.loadIgnorePatterns(ignoreFilePath); + this.loadStats(); + + // Tick active window duration every 5 seconds + this.durationTimer = setInterval(() => this.tickActiveDuration(), 5000); + } + + /** + * Checks if a given application path matches any pattern in .appignore + */ + public isIgnored(appPath: string): boolean { + if (!appPath || this.ignorePatterns.length === 0) return false; + return this.ignorePatterns.some((pattern) => pattern.test(appPath)); + } + + /** + * Handles OS focus switches between window applications. + */ + public async handleAppSwitch(event: AppSwitchEvent): Promise { + // Exclude ignored applications from focus tracking and launches + if (this.isIgnored(event.path)) { + return; + } + + const now = event.timestamp || Date.now(); + + // Register launch if a new PID is encountered + if (event.pid && this.registerPidLaunch(event.pid)) { + logger.info(`New process detected via PID ${event.pid}`); + this.recordAppLaunch(event.path); + } + + if (this.activePath === event.path) { + return; + } + + // Conclude duration calculation for the app that just lost focus + this.flushActiveDuration(now); + + // Update focus stats for newly focused application + const stat = this.getOrCreateStat(event.path); + stat.focusCount = (stat.focusCount || 0) + 1; + + this.activePath = event.path; + this.activeStartTime = now; + + this.queueSaveStats(); + this.queueUpdateSlots(); + } + + /** + * Records an explicit application launch event. + */ + public recordAppLaunch(appPath: string): void { + if (this.isIgnored(appPath)) { + return; + } + + const stat = this.getOrCreateStat(appPath); + stat.launchCount = (stat.launchCount || 0) + 1; + stat.lastUsedTimestamp = Date.now(); + + this.queueSaveStats(); + this.queueUpdateSlots(); + } + + /** + * Parses the .appignore file and converts lines into RegExp objects. + */ + private loadIgnorePatterns(ignoreFilePath: string): void { + try { + if (!fs.existsSync(ignoreFilePath)) { + logger.info(`.appignore file not found at "${ignoreFilePath}". Skipping ignore filtering.`); + return; + } + + const content = fs.readFileSync(ignoreFilePath, "utf-8"); + this.ignorePatterns = content + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("#")) + .map((pattern) => { + let flags = ""; + let cleanPattern = pattern; + + // Convert inline (?i) syntax to JavaScript RegExp 'i' flag + if (cleanPattern.startsWith("(?i)")) { + flags += "i"; + cleanPattern = cleanPattern.substring(4); + } + + return new RegExp(cleanPattern, flags); + }); + + logger.info(`Loaded ${this.ignorePatterns.length} ignore patterns from .appignore.`); + } catch (err) { + logger.error("Failed to load .appignore file:", err); + this.ignorePatterns = []; + } + } + + /** + * Synchronizes active Stream Deck AppSlotAction keys with ranked application stats. + */ + public async updateSlots(): Promise { + const activeActions = Array.from(AppSlotAction.activeActions.values()); + if (activeActions.length === 0) return; + + logger.info(`Updating Stream Deck slots for ${activeActions.length} active actions.`); + + for (const action of activeActions) { + if (!action.isKey()) continue; + + const settings = AppSlotAction.settingsMap.get(action.id) || {}; + const slotIndex = (settings.slotIndex ?? 1) - 1; + const metric = settings.metric || "focusCount"; + + const rankedApps = this.getRankedApps(metric); + logger.info(`Ranked apps by ${metric} for slot index ${slotIndex}: ${rankedApps.map(app => app.name).join(", ")}`); + const app = rankedApps[slotIndex]; + + if (app) { + await this.renderSlotWithApp(action.id, action, app); + } else { + await this.renderEmptySlot(action.id, action, slotIndex); + } + } + } + + public queueUpdateSlots(delayMs: number = 50): void { + if (this.updateTimer) clearTimeout(this.updateTimer); + this.updateTimer = setTimeout(() => { + void this.updateSlots(); + }, delayMs); + } + + public clearSlotCache(actionId: string): void { + this.renderedSlots.delete(actionId); + } + + public getRankedApps(metric: TrackedMetric): AppStat[] { + let list = Array.from(this.stats.values()); + + if (metric === "lastUsed") { + if (this.activePath) { + list = list.filter((app) => app.path !== this.activePath); + } + return list.sort((a, b) => b.lastUsedTimestamp - a.lastUsedTimestamp); + } + + const metricKeys: Record, keyof AppStat> = { + launchCount: "launchCount", + focusCount: "focusCount", + activeDuration: "activeDurationSeconds" + }; + + const key = metricKeys[metric as keyof typeof metricKeys] || "focusCount"; + return list.sort((a, b) => ((b[key] as number) || 0) - ((a[key] as number) || 0)); + } + + public applyDecay(decayFactor: number = 0.9): void { + logger.info(`Applying statistic decay factor (${decayFactor})...`); + for (const stat of this.stats.values()) { + stat.launchCount = Math.floor((stat.launchCount || 0) * decayFactor); + stat.focusCount = Math.floor((stat.focusCount || 0) * decayFactor); + stat.activeDurationSeconds = Math.floor(stat.activeDurationSeconds * decayFactor); + } + this.saveStatsImmediately(); + this.queueUpdateSlots(); + } + + public resetStats(): void { + logger.info("Resetting all application tracking data..."); + this.stats.clear(); + this.activePath = null; + this.renderedSlots.clear(); + this.saveStatsImmediately(); + this.queueUpdateSlots(); + } + + public destroy(): void { + if (this.durationTimer) clearInterval(this.durationTimer); + if (this.updateTimer) clearTimeout(this.updateTimer); + if (this.saveTimer) clearTimeout(this.saveTimer); + + this.flushActiveDuration(); + this.saveStatsImmediately(); + } + + // --- Helper Methods --- + +private getOrCreateStat(appPath: string): AppStat { + let stat = this.stats.get(appPath); + if (!stat) { + const baseExeName = path.basename(appPath, path.extname(appPath)); + + // Baseline object initialized immediately + stat = { + name: baseExeName, // Temporary fallback + path: appPath, + launchCount: 0, + focusCount: 0, + activeDurationSeconds: 0, + lastUsedTimestamp: 0 + }; + this.stats.set(appPath, stat); + + // Fetch Windows FileDescription metadata asynchronously + void AppMetadata.getFileDescription(appPath).then((description) => { + if (description && stat) { + stat.name = description; // Updates app-stats.json entry with real title + this.queueSaveStats(); + this.queueUpdateSlots(); + } + }); + + logger.info(`Started tracking new app: ${baseExeName}`); + } + return stat; + } + + private flushActiveDuration(now: number = Date.now()): void { + if (this.activePath && this.stats.has(this.activePath)) { + const elapsedSeconds = Math.max(0, Math.floor((now - this.activeStartTime) / 1000)); + if (elapsedSeconds > 0) { + const stat = this.stats.get(this.activePath)!; + stat.activeDurationSeconds += elapsedSeconds; + stat.lastUsedTimestamp = now; + this.activeStartTime = now; + } + } + } + + private tickActiveDuration(): void { + if (this.activePath) { + this.flushActiveDuration(); + this.queueSaveStats(); + } + } + + private registerPidLaunch(pid: number): boolean { + if (this.knownPids.has(pid)) return false; + + if (this.knownPids.size >= this.MAX_TRACKED_PIDS) { + const oldestPid = this.knownPids.values().next().value; + if (oldestPid !== undefined) { + this.knownPids.delete(oldestPid); + } + } + + this.knownPids.add(pid); + return true; + } + +private async renderSlotWithApp(actionId: string, action: any, app: AppStat): Promise { + // Step 1: Check AppNameMapper overrides first -> fallback to app.name (Metadata) + const rawTitle = this.nameMapper.resolveDisplayName(app.path, app.name); + + // Step 2: Apply line-wrapping / title shortening + const displayTitle = AppLauncher.shortenTitle(rawTitle); + const currentRendered = this.renderedSlots.get(actionId); + + if (currentRendered && currentRendered.appPath === app.path && currentRendered.title === displayTitle) { + return; + } + + const iconBase64 = await AppLauncher.getAppIcon(app.path); + AppSlotAction.appPathMap.set(actionId, app.path); + + await action.setTitle(displayTitle); + await action.setImage(iconBase64 ? `data:image/png;base64,${iconBase64}` : ""); + + this.renderedSlots.set(actionId, { appPath: app.path, title: displayTitle }); + } + + private async renderEmptySlot(actionId: string, action: any, slotIndex: number): Promise { + const emptyTitle = `Slot ${slotIndex + 1}\n(Empty)`; + const currentRendered = this.renderedSlots.get(actionId); + + if (currentRendered && currentRendered.appPath === "" && currentRendered.title === emptyTitle) { + return; + } + + AppSlotAction.appPathMap.delete(actionId); + await action.setTitle(emptyTitle); + await action.setImage(""); + + this.renderedSlots.set(actionId, { appPath: "", title: emptyTitle }); + } + + private queueSaveStats(delayMs: number = 1000): void { + if (this.saveTimer) return; + this.saveTimer = setTimeout(() => { + this.saveTimer = null; + this.saveStatsImmediately(); + }, delayMs); + } + + public saveStatsImmediately(): void { + try { + const dir = path.dirname(this.storageFilePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const rawObj = Object.fromEntries(this.stats); + fs.writeFileSync(this.storageFilePath, JSON.stringify(rawObj, null, 2), "utf-8"); + } catch (err) { + logger.error("Failed to save app stats file:", err); + } + } + + private loadStats(): void { + try { + if (fs.existsSync(this.storageFilePath)) { + const content = fs.readFileSync(this.storageFilePath, "utf-8"); + const rawObj: Record = JSON.parse(content); + this.stats = new Map(); + + for (const [key, val] of Object.entries(rawObj)) { + // Filter out any ignored entries stored historically + if (this.isIgnored(key)) continue; + + this.stats.set(key, { + ...val, + launchCount: val.launchCount ?? 0, + focusCount: val.focusCount ?? 0 + }); + } + logger.info(`Loaded tracking data for ${this.stats.size} apps.`); + } + } catch (err) { + logger.error("Failed to load app stats file:", err); + this.stats = new Map(); + } + } +} \ No newline at end of file diff --git a/dynamic-deck/src/services/blacklist-manager.ts b/dynamic-deck/src/services/blacklist-manager.ts new file mode 100644 index 0000000..d39f73b --- /dev/null +++ b/dynamic-deck/src/services/blacklist-manager.ts @@ -0,0 +1,71 @@ +// src/services/blacklist-manager.ts +import streamDeck from "@elgato/streamdeck"; +import * as fs from "fs"; +import * as path from "path"; + +const logger = streamDeck.logger.createScope("BlacklistManager"); + +export class BlacklistManager { + private ignorePatterns: RegExp[] = []; + + constructor(private ignoreFilePath: string) { + this.loadRules(); + this.watchFile(); + } + + private loadRules(): void { + try { + if (!fs.existsSync(this.ignoreFilePath)) { + const defaults = [ + "# App Ignore Blacklist", + "\\\\windows\\\\", + "powershell\\.exe$", + "cmd\\.exe$", + "explorer\\.exe$", + "applicationframehost\\.exe$", + "searchhost\\.exe$", + "streamdeck\\.exe$" + ].join("\n"); + + const dir = path.dirname(this.ignoreFilePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(this.ignoreFilePath, defaults, "utf-8"); + } + + const content = fs.readFileSync(this.ignoreFilePath, "utf-8"); + this.ignorePatterns = content + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .map((pattern) => { + // Clean inline (?i) flags if present from PowerShell syntax + const cleanedPattern = pattern.replace(/^\(\?i\)/i, ""); + return new RegExp(cleanedPattern, "i"); + }); + + logger.info(`Loaded ${this.ignorePatterns.length} blacklist rules.`); + } catch (err) { + logger.error("Failed to load ignore rules:", err); + } + } + + private watchFile(): void { + try { + fs.watch(this.ignoreFilePath, (eventType) => { + if (eventType === "change") { + logger.info("Blacklist file modified. Reloading rules..."); + this.loadRules(); + } + }); + } catch (err) { + logger.error("Failed to attach file watcher:", err); + } + } + + public isIgnored(appPath: string): boolean { + if (!appPath) return true; + return this.ignorePatterns.some((pattern) => pattern.test(appPath)); + } +} \ No newline at end of file diff --git a/dynamic-deck/src/ui/app-slot.html b/dynamic-deck/src/ui/app-slot.html new file mode 100644 index 0000000..b04f186 --- /dev/null +++ b/dynamic-deck/src/ui/app-slot.html @@ -0,0 +1,117 @@ + + + + + Dynamic App Slot Settings + + + +
+
Slot Position
+
+ +
+
+ +
+
Metric
+
+ +
+
+ +
+
Reset Data
+
+ +
+
+ + + + \ No newline at end of file diff --git a/dynamic-deck/src/ui/deepseek-balance.html b/dynamic-deck/src/ui/deepseek-balance.html new file mode 100644 index 0000000..e7d1e2c --- /dev/null +++ b/dynamic-deck/src/ui/deepseek-balance.html @@ -0,0 +1,13 @@ + + + + + DeepSeek Balance Settings + + + + + + + + \ No newline at end of file diff --git a/dynamic-deck/src/ui/ecobee.html b/dynamic-deck/src/ui/ecobee.html new file mode 100644 index 0000000..7ae2f3a --- /dev/null +++ b/dynamic-deck/src/ui/ecobee.html @@ -0,0 +1,47 @@ + + + + + Ecobee Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dynamic-deck/src/ui/launch-app.html b/dynamic-deck/src/ui/launch-app.html new file mode 100644 index 0000000..75af6fb --- /dev/null +++ b/dynamic-deck/src/ui/launch-app.html @@ -0,0 +1,150 @@ + + + + + Launch App Settings + + + + + + +
+
App:
+
+
+
+ + Scanning apps... +
+ ▼ +
+
+
+
+ +
+
Option:
+
+ +
+
+ + + + \ No newline at end of file diff --git a/dynamic-deck/src/ui/launch-app.js b/dynamic-deck/src/ui/launch-app.js new file mode 100644 index 0000000..209335e --- /dev/null +++ b/dynamic-deck/src/ui/launch-app.js @@ -0,0 +1,243 @@ +let websocket = null; +let pluginUUID = null; +let actionUUID = null; +let currentSettings = {}; +let appList = []; + +function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { + pluginUUID = inUUID; + const actionData = JSON.parse(inActionInfo); + actionUUID = actionData.action; + currentSettings = actionData.payload.settings || {}; + + if (currentSettings.bringToFront === undefined) { + currentSettings.bringToFront = true; + } + + updateInitialDisplay(); + + websocket = new WebSocket("ws://127.0.0.1:" + inPort); + + websocket.onopen = function () { + websocket.send(JSON.stringify({ + event: inRegisterEvent, + uuid: inUUID + })); + + sendToPlugin({ event: "getApps" }); + }; + + websocket.onmessage = function (evt) { + try { + const jsonObj = JSON.parse(evt.data); + const payload = jsonObj.payload || jsonObj; + + if (payload.event === "getAppsResponse") { + appList = Array.isArray(payload.apps) ? payload.apps : []; + renderDropdown(); + } else if (payload.event === "filePickedResponse" && payload.app) { + const customApp = payload.app; + + currentSettings.appPreset = customApp.path; + currentSettings.customApp = customApp; + if (payload.title) { + currentSettings.title = payload.title; + } + + saveSettings(); + renderDropdown(); + } else if (payload.event === "appSelectedResponse" && payload.title) { + currentSettings.title = payload.title; + saveSettings(); + } + } catch (err) { + console.error("Error parsing WebSocket message:", err); + } + }; +} + +function formatIconSrc(icon) { + if (!icon) return ""; + return icon.startsWith("data:") ? icon : "data:image/png;base64," + icon; +} + +function updateInitialDisplay() { + if (currentSettings.customApp && currentSettings.customApp.name) { + updateSelectedDisplay(currentSettings.customApp.name, currentSettings.customApp.icon); + } else if (currentSettings.appPreset) { + const parts = currentSettings.appPreset.split(/[\\/]/); + const fileName = parts[parts.length - 1] || currentSettings.appPreset; + updateSelectedDisplay(fileName, ""); + } else { + updateSelectedDisplay("Select an App...", ""); + } +} + +function sendToPlugin(payload) { + if (websocket && websocket.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify({ + action: actionUUID, + event: "sendToPlugin", + context: pluginUUID, + payload: payload + })); + } +} + +function saveSettings() { + if (websocket && websocket.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify({ + event: "setSettings", + context: pluginUUID, + payload: currentSettings + })); + } +} + +function renderDropdown() { + const menu = document.getElementById("optionsMenu"); + const display = document.getElementById("selectedDisplay"); + if (!menu || !display) return; + + menu.innerHTML = ""; + let foundSelected = false; + + appList.forEach(app => { + const opt = createOptionElement(app.name, app.path, app.icon, app); + menu.appendChild(opt); + + if (currentSettings.appPreset === app.path) { + updateSelectedDisplay(app.name, app.icon); + foundSelected = true; + } + }); + + if (currentSettings.customApp && currentSettings.customApp.path) { + const customApp = currentSettings.customApp; + const inSystemList = appList.some(a => a.path.toLowerCase() === customApp.path.toLowerCase()); + + if (!inSystemList) { + const customOpt = createOptionElement(customApp.name, customApp.path, customApp.icon, customApp); + menu.appendChild(customOpt); + + if (currentSettings.appPreset === customApp.path) { + updateSelectedDisplay(customApp.name, customApp.icon); + foundSelected = true; + } + } + } + + const separator = document.createElement("div"); + separator.className = "dropdown-separator"; + menu.appendChild(separator); + + const otherOption = createOptionElement("Other...", "other", "", null); + menu.appendChild(otherOption); + + if (!foundSelected) { + if (currentSettings.appPreset === "other" || !currentSettings.appPreset) { + updateSelectedDisplay("Select an App...", ""); + } + } +} + +function createOptionElement(name, value, iconBase64, fullAppObject) { + const div = document.createElement("div"); + div.className = "option-item"; + + if (iconBase64) { + const img = document.createElement("img"); + img.src = formatIconSrc(iconBase64); + div.appendChild(img); + } else { + const placeholder = document.createElement("div"); + placeholder.className = "icon-placeholder"; + div.appendChild(placeholder); + } + + const text = document.createElement("span"); + text.textContent = name; + div.appendChild(text); + + div.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMenu(false); + + if (value === "other") { + sendToPlugin({ event: "pickFile", settings: currentSettings }); + } else { + currentSettings.appPreset = value; + if (fullAppObject) { + currentSettings.customApp = fullAppObject; + } + updateSelectedDisplay(name, iconBase64); + saveSettings(); + + if (fullAppObject) { + sendToPlugin({ + event: "appSelected", + app: fullAppObject, + settings: currentSettings + }); + } + } + }); + + return div; +} + +function updateSelectedDisplay(name, iconBase64) { + const display = document.getElementById("selectedDisplay"); + if (!display) return; + + display.innerHTML = ""; + + if (iconBase64) { + const img = document.createElement("img"); + img.src = formatIconSrc(iconBase64); + display.appendChild(img); + } else { + const placeholder = document.createElement("div"); + placeholder.className = "icon-placeholder"; + display.appendChild(placeholder); + } + + const text = document.createElement("span"); + text.textContent = name; + display.appendChild(text); +} + +function toggleMenu(open) { + const menu = document.getElementById("optionsMenu"); + if (!menu) return; + if (open === undefined) { + menu.classList.toggle("open"); + } else if (open) { + menu.classList.add("open"); + } else { + menu.classList.remove("open"); + } +} + +document.addEventListener("DOMContentLoaded", () => { + const trigger = document.getElementById("selectTrigger"); + const bringToFrontInput = document.getElementById("bringToFront"); + + if (trigger) { + trigger.addEventListener("click", () => toggleMenu()); + } + + if (bringToFrontInput) { + bringToFrontInput.checked = currentSettings.bringToFront !== false; + bringToFrontInput.addEventListener("change", (e) => { + currentSettings.bringToFront = e.target.checked; + saveSettings(); + }); + } + + document.addEventListener("click", (e) => { + if (!e.target.closest(".sdpi-item-value")) { + toggleMenu(false); + } + }); +}); \ No newline at end of file diff --git a/dynamic-deck/src/ui/sdpi.css b/dynamic-deck/src/ui/sdpi.css new file mode 100644 index 0000000..e6407ed --- /dev/null +++ b/dynamic-deck/src/ui/sdpi.css @@ -0,0 +1,162 @@ +/* ui/sdpi.css - Official Elgato Property Inspector Baseline */ +:root { + --sdpi-font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, sans-serif; + --sdpi-font-size: 9pt; + --sdpi-bg-color: #2d2d2d; + --sdpi-text-color: #d8d8d8; + --sdpi-control-bg: #383838; + --sdpi-border-color: #1e1e1e; + --sdpi-active-color: #0078d4; + --sdpi-hover-bg: #444444; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow-x: hidden; +} + +body { + font-family: var(--sdpi-font-family); + font-size: var(--sdpi-font-size); + color: var(--sdpi-text-color); + background-color: var(--sdpi-bg-color); + padding: 16px 12px; + user-select: none; +} + +/* Custom SDPI Webkit Scrollbar Styling */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #1e1e1e; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #4a4a4a; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #666666; +} + +.sdpi-item { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.sdpi-item-label { + width: 90px; + text-align: right; + padding-right: 12px; + color: #9e9e9e; + font-size: 8.5pt; + flex-shrink: 0; +} + +.sdpi-item-value { + flex: 1; + position: relative; + min-width: 0; +} + +.sdpi-item-value input[type="text"], +.sdpi-item-value input[type="number"], +.sdpi-item-value select { + width: 100%; + height: 28px; + background-color: var(--sdpi-control-bg); + border: 1px solid var(--sdpi-border-color); + border-radius: 3px; + color: #e2e2e2; + padding: 0 8px; + outline: none; + font-family: inherit; + font-size: inherit; +} + +.sdpi-item-value input:focus, +.sdpi-item-value select:focus { + border-color: var(--sdpi-active-color); +} + +/* Custom Icon Dropdown Component */ +.custom-dropdown-container { + position: relative; + width: 100%; +} + +.dropdown-selected { + display: flex; + align-items: center; + gap: 8px; + height: 28px; + padding: 0 8px; + background-color: var(--sdpi-control-bg); + border: 1px solid var(--sdpi-border-color); + border-radius: 3px; + color: #e2e2e2; + cursor: pointer; +} + +.dropdown-selected:hover { + border-color: #555555; +} + +.dropdown-arrow { + margin-left: auto; + font-size: 8px; + color: #888888; +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 2px); + left: 0; + right: 0; + max-height: 180px; + overflow-y: auto; + background-color: #232323; + border: 1px solid var(--sdpi-border-color); + border-radius: 3px; + z-index: 1000; + display: none; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); +} + +.dropdown-menu.open { + display: block; +} + +.dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + cursor: pointer; + font-size: inherit; + color: var(--sdpi-text-color); +} + +.dropdown-item:hover { + background-color: var(--sdpi-hover-bg); +} + +.app-icon-small { + width: 16px; + height: 16px; + object-fit: contain; + flex-shrink: 0; +} \ No newline at end of file diff --git a/dynamic-deck/src/utils/app-launcher.ts b/dynamic-deck/src/utils/app-launcher.ts new file mode 100644 index 0000000..ed96613 --- /dev/null +++ b/dynamic-deck/src/utils/app-launcher.ts @@ -0,0 +1,674 @@ +// src/utils/app-launcher.ts +import * as path from "path"; +import * as fs from "fs"; +import koffi from "koffi"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import streamDeck from "@elgato/streamdeck"; + +const execFileAsync = promisify(execFile); + +export interface InstalledApp { + name: string; + path: string; + icon?: string; +} + +const EXCLUDE_KEYWORDS = [ + "uninstall", "un-install", "remove", + "documentation", "manual", "readme", "read me", "release notes", "help", "website", "visit", "license", + "reset", "restore", "unload", "cleanup", "clean up", "diagnostic", "troubleshoot", "fix", + "odbc", "data source", "component services", "computer management", "event viewer", "performance monitor", + "resource monitor", "system information", "task scheduler", "memory diagnostic", "defragment", + "configuration", "config", "setup", "wizard", "updater", "options" +]; + +// COM GUIDs for IShellLink & IPersistFile +const CLSID_ShellLink = { + Data1: 0x00021401, + Data2: 0x0000, + Data3: 0x0000, + Data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46] +}; + +const IID_IShellLinkW = { + Data1: 0x000214f9, + Data2: 0x0000, + Data3: 0x0000, + Data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46] +}; + +const IID_IPersistFile = { + Data1: 0x0000010b, + Data2: 0x0000, + Data3: 0x0000, + Data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46] +}; + +const logger = streamDeck.logger.createScope("AppLauncher"); + +const MAX_CACHE_SIZE = 100; + +let win32: any = null; + +if (process.platform === "win32") { + try { + const user32 = koffi.load("user32.dll"); + const kernel32 = koffi.load("kernel32.dll"); + const ole32 = koffi.load("ole32.dll"); + const comdlg32 = koffi.load("comdlg32.dll"); + + const CLSID = koffi.struct("CLSID", { + Data1: "uint32", + Data2: "uint16", + Data3: "uint16", + Data4: koffi.array("uint8", 8) + }); + + const OPENFILENAMEW = koffi.struct("OPENFILENAMEW", { + lStructSize: "uint32", + hwndOwner: "intptr", + hInstance: "intptr", + lpstrFilter: "intptr", + lpstrCustomFilter: "intptr", + nMaxCustFilter: "uint32", + nFilterIndex: "uint32", + lpstrFile: "intptr", + nMaxFile: "uint32", + lpstrFileTitle: "intptr", + nMaxFileTitle: "uint32", + lpstrInitialDir: "intptr", + lpstrTitle: "intptr", + Flags: "uint32", + nFileOffset: "uint16", + nFileExtension: "uint16", + lpstrDefExt: "intptr", + lCustData: "intptr", + lpfnHook: "intptr", + lpTemplateName: "intptr", + pvReserved: "intptr", + dwReserved: "uint32", + FlagsEx: "uint32" + }); + + const EnumWindows = user32.func("bool __stdcall EnumWindows(intptr_t lpEnumFunc, intptr_t lParam)"); + const GetWindowThreadProcessId = user32.func( + "uint32 __stdcall GetWindowThreadProcessId(intptr_t hWnd, _Out_ uint32_t *lpdwProcessId)" + ); + const IsWindowVisible = user32.func("bool __stdcall IsWindowVisible(intptr_t hWnd)"); + const ShowWindow = user32.func("bool __stdcall ShowWindow(intptr_t hWnd, int nCmdShow)"); + const SetForegroundWindow = user32.func("bool __stdcall SetForegroundWindow(intptr_t hWnd)"); + + const OpenProcess = kernel32.func( + "intptr_t __stdcall OpenProcess(uint32_t dwDesiredAccess, bool bInheritHandle, uint32_t dwProcessId)" + ); + const CloseHandle = kernel32.func("bool __stdcall CloseHandle(intptr_t hObject)"); + const QueryFullProcessImageNameW = kernel32.func( + "bool __stdcall QueryFullProcessImageNameW(intptr_t hProcess, uint32_t dwFlags, _Out_ char16_t *lpExeName, _Inout_ uint32_t *lpdwSize)" + ); + + const CoInitialize = ole32.func("int32 __stdcall CoInitialize(intptr_t pvReserved)"); + const CoUninitialize = ole32.func("void __stdcall CoUninitialize()"); + const CoCreateInstance = ole32.func( + "int32 __stdcall CoCreateInstance(const CLSID *rclsid, intptr_t pUnkOuter, uint32_t dwClsContext, const CLSID *riid, _Out_ intptr_t *ppv)" + ); + + const GetOpenFileNameW = comdlg32.func("bool __stdcall GetOpenFileNameW(_Inout_ OPENFILENAMEW *lpofn)"); + + const EnumWindowsProc = koffi.proto("bool __stdcall EnumWindowsProc(intptr_t hWnd, intptr_t lParam)"); + + const ComReleaseProto = koffi.proto("uint32 __stdcall ComReleaseProto(intptr_t self)"); + const QueryInterfaceProto = koffi.proto("int32 __stdcall QueryInterfaceProto(intptr_t self, const CLSID *riid, _Out_ intptr_t *ppv)"); + const IPersistFileLoadProto = koffi.proto("int32 __stdcall IPersistFileLoadProto(intptr_t self, str16 pszFileName, uint32_t dwMode)"); + const IShellLinkGetPathProto = koffi.proto("int32 __stdcall IShellLinkGetPathProto(intptr_t self, void *pszFile, int cch, intptr_t pfd, uint32_t fFlags)"); + + win32 = { + EnumWindows, + EnumWindowsProc, + GetWindowThreadProcessId, + IsWindowVisible, + ShowWindow, + SetForegroundWindow, + OpenProcess, + CloseHandle, + QueryFullProcessImageNameW, + CoInitialize, + CoUninitialize, + CoCreateInstance, + GetOpenFileNameW, + CLSID, + OPENFILENAMEW, + ComReleaseProto, + QueryInterfaceProto, + IPersistFileLoadProto, + IShellLinkGetPathProto + }; + } catch (err) { + console.error("Failed to initialize Koffi Win32 bindings:", err); + } +} + +let targetSearchPath = ""; +let searchFoundHwnd: any = null; +let enumWindowsCallback: any = null; + +function getEnumWindowsCallback() { + if (!enumWindowsCallback && win32) { + enumWindowsCallback = koffi.register((hWnd: any) => { + if (!win32.IsWindowVisible(hWnd)) return true; + + const pidBuf = [0]; + win32.GetWindowThreadProcessId(hWnd, pidBuf); + const pid = pidBuf[0]; + if (!pid) return true; + + const hProcess = win32.OpenProcess(0x1000, false, pid); // PROCESS_QUERY_LIMITED_INFORMATION + if (!hProcess) return true; + + const pathBuf = new Uint16Array(260); + const sizeBuf = [260]; + const success = win32.QueryFullProcessImageNameW(hProcess, 0, pathBuf, sizeBuf); + win32.CloseHandle(hProcess); + + if (success) { + const procPath = Buffer.from(pathBuf.buffer, pathBuf.byteOffset, sizeBuf[0] * 2) + .toString("utf16le") + .toLowerCase(); + + const targetExe = targetSearchPath.split("\\").pop() || ""; + if (procPath === targetSearchPath || procPath.endsWith("\\" + targetExe)) { + searchFoundHwnd = hWnd; + return false; + } + } + return true; + }, koffi.pointer(win32.EnumWindowsProc)); + } + return enumWindowsCallback; +} + +export class AppLauncher { + private static cachedApps: InstalledApp[] = []; + private static iconCache = new Map(); + private static failedPaths = new Set(); + + + static shortenTitle(name: string): string { + if (!name) return ""; + const cleanName = name.replace(/\.exe$/i, "").trim(); + const words = cleanName.split(/\s+/); + if (words.length > 2) { + return words.slice(0, 2).join(" "); + } + return cleanName; + } + + private static releaseComObject(pUnk: number): void { + if (!pUnk || !win32) return; + try { + const vtableAddr = koffi.decode(pUnk, "intptr_t"); + if (!vtableAddr) return; + const ptrSize = process.arch === "x64" ? 8n : 4n; + const releaseAddr = koffi.decode(BigInt(vtableAddr) + 2n * ptrSize, "intptr_t"); + if (!releaseAddr) return; + const releaseFn = koffi.decode(releaseAddr, win32.ComReleaseProto); + releaseFn(pUnk); + } catch {} + } + + private static resolveShortcutTarget(lnkPath: string): string { + if (!win32) return ""; + + let pShellLink = 0; + let pPersistFile = 0; + + try { + const pShellLinkBuf = [0]; + const hr = win32.CoCreateInstance(CLSID_ShellLink, 0, 1, IID_IShellLinkW, pShellLinkBuf); + if (hr < 0 || !pShellLinkBuf[0]) return ""; + pShellLink = pShellLinkBuf[0]; + + const ptrSize = process.arch === "x64" ? 8n : 4n; + + const vtableLinkAddr = koffi.decode(pShellLink, "intptr_t"); + if (!vtableLinkAddr) return ""; + const vtableLinkBigInt = BigInt(vtableLinkAddr); + + const qiAddr = koffi.decode(vtableLinkBigInt, "intptr_t"); + if (!qiAddr) return ""; + const qiFn = koffi.decode(qiAddr, win32.QueryInterfaceProto); + + const pPersistFileBuf = [0]; + if (qiFn(pShellLink, IID_IPersistFile, pPersistFileBuf) < 0 || !pPersistFileBuf[0]) { + return ""; + } + pPersistFile = pPersistFileBuf[0]; + + const vtablePersistAddr = koffi.decode(pPersistFile, "intptr_t"); + if (!vtablePersistAddr) return ""; + const vtablePersistBigInt = BigInt(vtablePersistAddr); + + const loadAddr = koffi.decode(vtablePersistBigInt + 5n * ptrSize, "intptr_t"); + if (!loadAddr) return ""; + const loadFn = koffi.decode(loadAddr, win32.IPersistFileLoadProto); + + if (loadFn(pPersistFile, lnkPath, 0) < 0) { + return ""; + } + + const getPathAddr = koffi.decode(vtableLinkBigInt + 3n * ptrSize, "intptr_t"); + if (!getPathAddr) return ""; + const getPathFn = koffi.decode(getPathAddr, win32.IShellLinkGetPathProto); + + const targetBuf = Buffer.alloc(520); + if (getPathFn(pShellLink, targetBuf, 260, 0, 0) >= 0) { + return targetBuf.toString("utf16le").split("\0")[0].trim(); + } + } catch (err) { + logger.error(`Failed to resolve shortcut ${lnkPath}:`, err); + } finally { + if (pPersistFile) this.releaseComObject(pPersistFile); + if (pShellLink) this.releaseComObject(pShellLink); + } + + return ""; + } + + private static setCache(path: string, base64: string): void { + if (this.iconCache.size >= MAX_CACHE_SIZE) { + const firstKey = this.iconCache.keys().next().value; + if (firstKey) this.iconCache.delete(firstKey); + } + this.iconCache.set(path, base64); + } + +static async activateOrLaunchApp(targetPath: string, bringToFront = true): Promise { + if (process.platform !== "win32") return; + + const cleanPath = targetPath.trim().replace(/^"+|"+$/g, ""); + if (!cleanPath) return; + + const isUrlShortcut = cleanPath.toLowerCase().endsWith(".url"); + let activated = false; + + // Window enumeration matching is skipped for .url files, as shortcut paths do not match process window paths + if (!isUrlShortcut && bringToFront && win32) { + const cb = getEnumWindowsCallback(); + if (cb) { + targetSearchPath = cleanPath.toLowerCase().replace(/\//g, "\\"); + searchFoundHwnd = null; + + win32.EnumWindows(cb, 0); + + if (searchFoundHwnd) { + win32.ShowWindow(searchFoundHwnd, 9); // SW_RESTORE + win32.SetForegroundWindow(searchFoundHwnd); + activated = true; + } + } + } + + if (!activated) { + if (isUrlShortcut) { + logger.info(`[AppLauncher] Launching URL shortcut at path "${cleanPath}"`); + execFile("cmd.exe", ["/c", "start", "", cleanPath], (err: Error | null) => { + if (err) { + streamDeck.logger.error(`[AppLauncher] Failed to launch URL shortcut at path "${cleanPath}":`, err); + } + }); + } else { + logger.info(`[AppLauncher] Launching executable at path "${cleanPath}"`); + execFile(cleanPath, (err: Error | null) => { + if (err) { + streamDeck.logger.error(`[AppLauncher] Failed to launch executable at path "${cleanPath}":`, err); + } + }); + } + } +} + + static async getAppIcon(targetPath: string): Promise { + if (!targetPath || process.platform !== "win32") return ""; + + const cleanPath = path.normalize(targetPath.trim().replace(/^"+|"+$/g, "")); + + if (this.iconCache.has(cleanPath)) return this.iconCache.get(cleanPath)!; + if (this.failedPaths.has(cleanPath)) return ""; + + if (!fs.existsSync(cleanPath)) { + this.failedPaths.add(cleanPath); + return ""; + } + + try { + logger.info(`[getAppIcon] Extracting 128x128 icon via PowerShell for: "${cleanPath}"`); + + const escapedPath = cleanPath.replace(/'/g, "''"); + + const psScript = ` + Add-Type -AssemblyName System.Drawing + + $code = @' + using System; + using System.Drawing; + using System.Drawing.Drawing2D; + using System.Drawing.Imaging; + using System.Runtime.InteropServices; + + public class IconHelper { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern uint PrivateExtractIcons( + string lpszFile, int nIconIndex, int cxIcon, int cyIcon, + IntPtr[] phicon, uint[] piconid, uint nIcons, uint flags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyIcon(IntPtr hIcon); + + public static Bitmap GetIcon128(string filePath) { + IntPtr[] phicon = new IntPtr[1]; + uint[] piconid = new uint[1]; + + // Extract icon attempting 128x128 target size + uint count = PrivateExtractIcons(filePath, 0, 128, 128, phicon, piconid, 1, 0); + + Bitmap bmp = null; + if (count > 0 && phicon[0] != IntPtr.Zero) { + try { + using (Icon ic = Icon.FromHandle(phicon[0])) { + bmp = ic.ToBitmap(); + } + } finally { + DestroyIcon(phicon[0]); + } + } + + // Fallback to standard associated icon if extraction returns null + if (bmp == null) { + using (Icon ic = Icon.ExtractAssociatedIcon(filePath)) { + if (ic != null) bmp = ic.ToBitmap(); + } + } + + if (bmp == null) return null; + + // Ensure exact 128x128 canvas with high-quality resampling + if (bmp.Width == 128 && bmp.Height == 128) return bmp; + + Bitmap resized = new Bitmap(128, 128); + using (Graphics g = Graphics.FromImage(resized)) { + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.SmoothingMode = SmoothingMode.HighQuality; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.DrawImage(bmp, 0, 0, 128, 128); + } + bmp.Dispose(); + return resized; + } + } +'@ + + if (-not ([System.Management.Automation.PSTypeName]'IconHelper').Type) { + Add-Type -TypeDefinition $code -ReferencedAssemblies System.Drawing + } + + $bmp = [IconHelper]::GetIcon128('${escapedPath}') + if ($bmp) { + $ms = New-Object System.IO.MemoryStream + $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) + [System.Convert]::ToBase64String($ms.ToArray()) + $bmp.Dispose() + $ms.Dispose() + } + `.trim(); + + const { stdout } = await execFileAsync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + psScript + ], { maxBuffer: 10 * 1024 * 1024 }); + + const base64 = stdout.trim(); + + if (base64) { + this.setCache(cleanPath, base64); + return base64; + } else { + this.failedPaths.add(cleanPath); + return ""; + } + } catch (err) { + logger.error(`[getAppIcon] Exception extracting icon for "${cleanPath}":`, err); + this.failedPaths.add(cleanPath); + return ""; + } + } + +public static async getInstalledApps(): Promise { + if (this.cachedApps.length > 0) { + return this.cachedApps; + } + + if (process.platform !== "win32") return []; + + const psScript = ` + Add-Type -AssemblyName System.Drawing + + $code = @' + using System; + using System.Drawing; + using System.Drawing.Imaging; + using System.IO; + using System.Runtime.InteropServices; + + public class IconExtractor { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern uint PrivateExtractIcons( + string lpszFile, int nIconIndex, int cxIcon, int cyIcon, + IntPtr[] phicon, uint[] piconid, uint nIcons, uint flags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyIcon(IntPtr hIcon); + + public static string GetBase64Icon(string filePath) { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) return ""; + + string targetFile = filePath; + int iconIndex = 0; + + // Parse .url files to extract the embedded IconFile and IconIndex + if (filePath.EndsWith(".url", StringComparison.OrdinalIgnoreCase)) { + try { + string[] lines = File.ReadAllLines(filePath); + foreach (string line in lines) { + if (line.StartsWith("IconFile=", StringComparison.OrdinalIgnoreCase)) { + targetFile = line.Substring(9).Trim(); + } + if (line.StartsWith("IconIndex=", StringComparison.OrdinalIgnoreCase)) { + int.TryParse(line.Substring(10).Trim(), out iconIndex); + } + } + } catch {} + } + + IntPtr[] phicon = new IntPtr[1]; + uint[] piconid = new uint[1]; + uint count = PrivateExtractIcons(targetFile, iconIndex, 32, 32, phicon, piconid, 1, 0); + + Bitmap bmp = null; + if (count > 0 && phicon[0] != IntPtr.Zero) { + try { + using (Icon ic = Icon.FromHandle(phicon[0])) { + bmp = ic.ToBitmap(); + } + } finally { + DestroyIcon(phicon[0]); + } + } + + if (bmp == null) { + try { + using (Icon ic = Icon.ExtractAssociatedIcon(filePath)) { + if (ic != null) bmp = ic.ToBitmap(); + } + } catch {} + } + + if (bmp == null) return ""; + + using (MemoryStream ms = new MemoryStream()) { + bmp.Save(ms, ImageFormat.Png); + string base64 = Convert.ToBase64String(ms.ToArray()); + bmp.Dispose(); + return base64; + } + } + } +'@ + + if (-not ([System.Management.Automation.PSTypeName]'IconExtractor').Type) { + Add-Type -TypeDefinition $code -ReferencedAssemblies System.Drawing + } + + $shell = New-Object -ComObject WScript.Shell + + # Include Start Menu paths and Desktop locations (User & Public Desktop) + $searchPaths = @( + "$env:ProgramData\\Microsoft\\Windows\\Start Menu\\Programs", + "$env:AppData\\Microsoft\\Windows\\Start Menu\\Programs", + [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::CommonDesktopDirectory), + [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::DesktopDirectory) + ) + + $apps = @() + $seenPaths = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + + # Scan for both .lnk and .url shortcuts + Get-ChildItem -Path $searchPaths -Recurse -Include *.lnk, *.url -ErrorAction SilentlyContinue | ForEach-Object { + try { + $file = $_ + if ($file.Extension -eq ".url") { + $targetPath = $file.FullName + if ($seenPaths.Add($targetPath)) { + $iconBase64 = [IconExtractor]::GetBase64Icon($targetPath) + $apps += [PSCustomObject]@{ + name = $file.BaseName + path = $targetPath + icon = $iconBase64 + } + } + } else { + $shortcut = $shell.CreateShortcut($file.FullName) + $targetPath = $shortcut.TargetPath + + if ($targetPath -and $targetPath.EndsWith(".exe") -and (Test-Path $targetPath)) { + if ($seenPaths.Add($targetPath)) { + $iconBase64 = [IconExtractor]::GetBase64Icon($targetPath) + $apps += [PSCustomObject]@{ + name = $file.BaseName + path = $targetPath + icon = $iconBase64 + } + } + } + } + } catch {} + } + + $apps | Sort-Object name | ConvertTo-Json -Compress + `.trim(); + + try { + const { stdout } = await execFileAsync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + psScript + ], { maxBuffer: 20 * 1024 * 1024 }); + + if (!stdout.trim()) return []; + + const parsed: InstalledApp[] = JSON.parse(stdout.trim()); + const apps: InstalledApp[] = Array.isArray(parsed) ? parsed : [parsed]; + + const uniquePaths = new Set(); + const uniqueNames = new Set(); + const filteredApps: InstalledApp[] = []; + for (const app of apps) { + if (!app || !app.name || !app.path) continue; + + const lowerName = app.name.toLowerCase().trim(); + const lowerPath = app.path.toLowerCase().trim(); + + if (EXCLUDE_KEYWORDS.some((keyword) => lowerName.includes(keyword))) continue; + if (lowerPath.includes("\\windows\\system32\\") || lowerPath.includes("\\windows\\syswow64\\")) continue; + if (uniquePaths.has(lowerPath) || uniqueNames.has(lowerName)) continue; + + uniquePaths.add(lowerPath); + uniqueNames.add(lowerName); + filteredApps.push(app); + } + + this.cachedApps = filteredApps.sort((a, b) => a.name.localeCompare(b.name)); + return this.cachedApps; + } catch (err) { + console.error("Failed to get installed apps with icons:", err); + return []; + } +} + + static async openExecutablePicker(): Promise { + if (process.platform !== "win32" || !win32) return null; + + try { + const filterStr = "Executable Files (*.exe)\0*.exe\0All Files (*.*)\0*.*\0\0"; + const filterBuf = Buffer.from(filterStr, "ucs2"); + + const fileBuf = Buffer.alloc(520); + const titleStr = "Select Application Executable\0"; + const titleBuf = Buffer.from(titleStr, "ucs2"); + + const ofn = { + lStructSize: koffi.sizeof(win32.OPENFILENAMEW), + hwndOwner: 0, + hInstance: 0, + lpstrFilter: koffi.address(filterBuf), + lpstrCustomFilter: 0, + nMaxCustFilter: 0, + nFilterIndex: 1, + lpstrFile: koffi.address(fileBuf), + nMaxFile: 260, + lpstrFileTitle: 0, + nMaxFileTitle: 0, + lpstrInitialDir: 0, + lpstrTitle: koffi.address(titleBuf), + Flags: 0x00000800 | 0x00000100 | 0x00000008, + nFileOffset: 0, + nFileExtension: 0, + lpstrDefExt: 0, + lCustData: 0, + lpfnHook: 0, + lpTemplateName: 0, + pvReserved: 0, + dwReserved: 0, + FlagsEx: 0 + }; + + const success = win32.GetOpenFileNameW(ofn); + + if (success) { + const targetPath = fileBuf.toString("utf16le").split("\0")[0].trim(); + + if (targetPath && fs.existsSync(targetPath)) { + const name = path.basename(targetPath); + const icon = await this.getAppIcon(targetPath); + return { path: targetPath, name, icon }; + } + } + } catch (err) { + logger.error("Exception in openExecutablePicker:", err); + } + + return null; + } +} \ No newline at end of file diff --git a/dynamic-deck/src/utils/app-metadata.ts b/dynamic-deck/src/utils/app-metadata.ts new file mode 100644 index 0000000..740b424 --- /dev/null +++ b/dynamic-deck/src/utils/app-metadata.ts @@ -0,0 +1,31 @@ +import { execFile } from "child_process"; +import * as path from "path"; + +export class AppMetadata { + /** + * Reads the FileDescription or ProductName from Windows executable VersionInfo. + */ + public static async getFileDescription(exePath: string): Promise { + return new Promise((resolve) => { + // Escaped command querying FileDescription, falling back to ProductName + const psScript = ` + $info = (Get-Item -LiteralPath '${exePath.replace(/'/g, "''")}').VersionInfo; + if ($info.FileDescription) { $info.FileDescription } else { $info.ProductName } + `; + + execFile( + "powershell.exe", + ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", psScript], + { timeout: 3000 }, + (error, stdout) => { + if (error || !stdout) { + resolve(null); + return; + } + const description = stdout.trim(); + resolve(description.length > 0 ? description : null); + } + ); + }); + } +} \ No newline at end of file diff --git a/dynamic-deck/src/utils/app-name-mapper.ts b/dynamic-deck/src/utils/app-name-mapper.ts new file mode 100644 index 0000000..78439fb --- /dev/null +++ b/dynamic-deck/src/utils/app-name-mapper.ts @@ -0,0 +1,74 @@ +import * as fs from "fs"; +import * as path from "path"; +import streamDeck from "@elgato/streamdeck"; + +const logger = streamDeck.logger.createScope("AppNameMapper"); + +interface PatternRule { + pattern: RegExp; + replacement: string; +} + +export class AppNameMapper { + private exactMap: Record = {}; + private patternRules: PatternRule[] = []; + + + constructor(private configPath: string = path.join(process.cwd(), "app-names.json")) { + this.loadConfig(); + } + + public loadConfig(): void { + if (!fs.existsSync(this.configPath)) return; + + try { + const raw = fs.readFileSync(this.configPath, "utf-8"); + const data = JSON.parse(raw); + + this.exactMap = Object.fromEntries( + Object.entries(data.exact || {}).map(([k, v]) => [k.toLowerCase(), v as string]) + ); + + this.patternRules = (data.patterns || []).map((p: any) => { + let flags = p.flags || ""; + let regexStr = p.regex; + if (regexStr.startsWith("(?i)")) { + flags += "i"; + regexStr = regexStr.substring(4); + } + return { + pattern: new RegExp(regexStr, flags), + replacement: p.replacement + }; + }); + } catch (err) { + console.error("Failed to parse app-names.json:", err); + } + } + + /** + * Resolves display title: + * 1. Check exact json override + * 2. Check regex pattern override + * 3. Fallback to fallbackName (Metadata from app-stats.json) + */ + public resolveDisplayName(appPath: string, fallbackName: string): string { + const exeName = path.basename(appPath).toLowerCase(); + + // 1. Check exact match in JSON + if (this.exactMap[exeName]) { + return this.exactMap[exeName]; + } + + // 2. Check regex patterns in JSON + for (const rule of this.patternRules) { + logger.info(`checking pattern ${rule.pattern} against ${exeName}`); + if (rule.pattern.test(exeName)) { + return exeName.replace(rule.pattern, rule.replacement); + } + } + + // 3. Fallback to stored metadata name + return fallbackName; + } +} \ No newline at end of file diff --git a/dynamic-deck/tsconfig.json b/dynamic-deck/tsconfig.json new file mode 100644 index 0000000..0786510 --- /dev/null +++ b/dynamic-deck/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "customConditions": [ + "node" + ], + "module": "ES2022", + "moduleResolution": "Bundler", + "noImplicitOverride": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d4f4578 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1000 @@ +{ + "name": "streamdeck", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@elgato/cli": "^1.9.0" + } + }, + "node_modules/@elgato/cli": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@elgato/cli/-/cli-1.9.0.tgz", + "integrity": "sha512-svzkUhR5+4R2x1fiY05b2s2rEnCs64O9Tg/s/BHByxfimDcjWn1laKT3rDvUrmxgO44p3+w6ItSrk+uijOB05w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@elgato/schemas": "^0.4.14", + "@humanwhocodes/momoa": "^3.0.0", + "@zip.js/zip.js": "^2.7.54", + "ajv": "^8.17.1", + "chalk": "^5.4.1", + "commander": "^13.0.0", + "ejs": "^3.1.10", + "find-process": "^1.4.10", + "ignore": "^7.0.3", + "inquirer": "^12.3.2", + "is-interactive": "^2.0.0", + "lodash": "^4.17.21", + "log-symbols": "^7.0.0", + "rage-edit": "^1.2.0", + "semver": "^7.6.3" + }, + "bin": { + "sd": "bin/streamdeck.mjs", + "streamdeck": "bin/streamdeck.mjs" + }, + "engines": { + "node": ">=20.1.0" + } + }, + "node_modules/@elgato/schemas": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.4.16.tgz", + "integrity": "sha512-PFEi6F36ImO5KCSNHui9o/uXkwjca3hk5b95Df7LgNBdQYREXT3B+P3tYoz6avRuOPlArkKFSkOOvH6RRpjvLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.12.tgz", + "integrity": "sha512-xS9xl4ieqTqhSZfKV3YXj/htwJCUxbgvkTVaK1fkESgrOzvoVSOTRQeQ8tTLOZUS0Csi2xqtJQXgVTRH5OEbHQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.57", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.57.tgz", + "integrity": "sha512-cd5a0s7CS0MOD52f7FW4ju84wc/+b+YNu5XIG72PfXRLk6rInEdvIrB/BjR8D8hS4fMKuYJOBjv6FoLh3e7DHw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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": "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/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/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/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/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/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-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/find-process": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz", + "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/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/find-process/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "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/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/rage-edit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rage-edit/-/rage-edit-1.2.0.tgz", + "integrity": "sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/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/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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..227e026 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@elgato/cli": "^1.9.0" + } +}