diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml deleted file mode 100644 index 494e68b..0000000 --- a/.github/workflows/electron-build.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: Electron Build - -on: - push: - branches: [ main ] - tags: [ 'v*' ] - pull_request: - branches: [ main ] - workflow_dispatch: - inputs: - version: - description: 'Version to build (optional)' - required: false - type: string - -jobs: - build-electron: - runs-on: ${{ matrix.os }} - permissions: - contents: write - - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - include: - - os: ubuntu-latest - platform: linux - - os: windows-latest - platform: win32 - - os: macos-latest - platform: darwin - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: src/CommandRunner.ReactWebsite/package-lock.json - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Install dependencies - working-directory: src/CommandRunner.ReactWebsite - run: npm ci - - - name: Build React app - working-directory: src/CommandRunner.ReactWebsite - run: npm run build-electron-build - - - name: Build Electron app (Linux) - if: matrix.platform == 'linux' - working-directory: src/CommandRunner.ReactWebsite - run: | - echo "Building Linux Electron app..." - echo "This will create both AppImage and .deb packages" - echo "Note: AppImage may have sandbox issues - use .deb for better compatibility" - npm run build-electron-linux - - - name: Build Electron app (Windows) - if: matrix.platform == 'win32' - working-directory: src/CommandRunner.ReactWebsite - run: | - echo "Building Windows Electron app..." - echo "This will create a .exe installer" - npm run build-electron-win - - - name: Build Electron app (macOS) - if: matrix.platform == 'darwin' - working-directory: src/CommandRunner.ReactWebsite - run: | - echo "Building macOS Electron app..." - echo "This will create a .dmg installer" - npm run build-electron-mac - - - name: Verify build output (Linux) - if: matrix.platform == 'linux' - run: | - echo "=== LINUX BUILD VERIFICATION ===" - echo "Build output directory:" - ls -la src/CommandRunner.ReactWebsite/dist-electron/ - echo "" - echo "Package files found:" - find src/CommandRunner.ReactWebsite/dist-electron/ -name "*.deb" -o -name "*.AppImage" -o -name "*Runner.sh" -o -name "*Runner.desktop" -o -name "*.md" | head -10 - echo "" - echo "Package sizes:" - echo "AppImage (may have sandbox issues):" - du -sh "src/CommandRunner.ReactWebsite/dist-electron/CommandRunner-0.0.0.AppImage" 2>/dev/null || echo " AppImage not found" - echo ".deb package (recommended for Linux):" - du -sh "src/CommandRunner.ReactWebsite/dist-electron/commandrunner-reactwebsite_0.0.0_amd64.deb" 2>/dev/null || echo " .deb not found" - echo "Wrapper script (alternative for AppImage):" - du -sh "src/CommandRunner.ReactWebsite/dist-electron/CommandRunner.sh" 2>/dev/null || echo " Wrapper script not found" - echo "Desktop file (for system integration):" - du -sh "src/CommandRunner.ReactWebsite/dist-electron/CommandRunner.desktop" 2>/dev/null || echo " Desktop file not found" - echo "Linux README (installation instructions):" - ls -la "src/CommandRunner.ReactWebsite/dist-electron/README-Linux.md" 2>/dev/null || echo " README not found" - echo "" - echo "=== RECOMMENDATION ===" - echo "For Linux users: Use the .deb package or the .sh wrapper script" - echo "The .deb package doesn't have sandbox issues and integrates better with the system" - - - name: Verify build output (Windows) - if: matrix.platform == 'win32' - shell: pwsh - run: | - Write-Host "=== WINDOWS BUILD VERIFICATION ===" - Write-Host "Build output directory:" - Get-ChildItem src/CommandRunner.ReactWebsite/dist-electron/ - Write-Host "" - Write-Host "Windows installer files:" - Get-ChildItem src/CommandRunner.ReactWebsite/dist-electron/ -Name *.exe | Select-Object -First 10 - Write-Host "" - Write-Host "Build sizes:" - Get-ChildItem src/CommandRunner.ReactWebsite/dist-electron/*.exe | ForEach-Object { - $sizeMB = [math]::Round($_.Length / 1MB, 2) - Write-Host "$sizeMB MB - $($_.Name)" - } - Write-Host "" - Write-Host "=== WINDOWS INSTALLER INFO ===" - Write-Host "The .exe file is a standard Windows installer" - Write-Host "It includes the API server and will auto-start it when the app runs" - - - name: Verify build output (macOS) - if: matrix.platform == 'darwin' - run: | - echo "=== MACOS BUILD VERIFICATION ===" - echo "Build output directory:" - ls -la src/CommandRunner.ReactWebsite/dist-electron/ - echo "" - echo "macOS installer files:" - find src/CommandRunner.ReactWebsite/dist-electron/ -name "*.dmg" -o -name "*.app" | head -10 - echo "" - echo "Package sizes:" - du -sh src/CommandRunner.ReactWebsite/dist-electron/*.dmg 2>/dev/null || echo " DMG not found" - echo "" - echo "=== MACOS INSTALLER INFO ===" - echo "The .dmg file is a standard macOS disk image" - echo "It includes the API server and will auto-start it when the app runs" - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: command-runner-${{ matrix.platform }}-${{ github.run_number }} - path: | - src/CommandRunner.ReactWebsite/dist-electron/ - retention-days: 30 - - - name: Create Pre-release (on PR) - if: github.event_name == 'pull_request' - uses: softprops/action-gh-release@v1 - with: - tag_name: pr-${{ github.event.pull_request.number }}-${{ matrix.platform }}-${{ github.run_number }} - name: PR #${{ github.event.pull_request.number }} pre-release (${{ matrix.platform }}) - target_commitish: ${{ github.sha }} - files: | - src/CommandRunner.ReactWebsite/dist-electron/*.exe - src/CommandRunner.ReactWebsite/dist-electron/*.deb - src/CommandRunner.ReactWebsite/dist-electron/*.AppImage - src/CommandRunner.ReactWebsite/dist-electron/*.dmg - generate_release_notes: false - draft: false - prerelease: true - overwrite_files: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Release (on tag push) - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v1 - with: - files: | - src/CommandRunner.ReactWebsite/dist-electron/*.exe - src/CommandRunner.ReactWebsite/dist-electron/*.deb - src/CommandRunner.ReactWebsite/dist-electron/*.AppImage - src/CommandRunner.ReactWebsite/dist-electron/*.dmg - generate_release_notes: true - draft: false - prerelease: false - overwrite_files: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - cleanup-workflow-run-logs: - needs: build-electron - runs-on: ubuntu-latest - permissions: - actions: write - steps: - - name: Cleanup workflow run logs - uses: igorjs/gh-actions-clean-workflow@v4 - with: - days_old: "7" - runs_to_keep: "5" diff --git a/.github/workflows/photino-build.yml b/.github/workflows/photino-build.yml new file mode 100644 index 0000000..a152692 --- /dev/null +++ b/.github/workflows/photino-build.yml @@ -0,0 +1,145 @@ +name: Photino Build + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + version: + description: 'Version to build (optional)' + required: false + type: string + +jobs: + build-photino: + runs-on: ${{ matrix.os }} + permissions: + contents: write + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + platform: linux + rid: linux-x64 + - os: windows-latest + platform: win32 + rid: win-x64 + - os: macos-latest + platform: darwin + rid: osx-x64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: src/CommandRunner.Website.VueJs/package-lock.json + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install Vue dependencies + working-directory: src/CommandRunner.Website.VueJs + run: npm ci + + - name: Build Vue app + working-directory: src/CommandRunner.Website.VueJs + run: npm run build + + - name: Publish Photino desktop host (${{ matrix.rid }}) + run: | + dotnet publish src/CommandRunner.Desktop -c Release -r ${{ matrix.rid }} --self-contained true -o publish/${{ matrix.platform }} + + - name: Verify wwwroot was bundled + shell: bash + run: | + if [ ! -f "publish/${{ matrix.platform }}/wwwroot/index.html" ]; then + echo "wwwroot/index.html missing from publish output -- Vue build didn't get bundled" + exit 1 + fi + echo "wwwroot bundled correctly:" + ls publish/${{ matrix.platform }}/wwwroot + + - name: Package build (Linux) + if: matrix.platform == 'linux' + run: | + cd publish/linux + tar czf ../../CommandRunner-linux-x64.tar.gz . + cd ../.. + echo "Note: running the app requires libwebkit2gtk-4.1-0 (or libwebkit2gtk-4.0-37 on older distros) installed on the target machine -- it is not bundled, same as any WebKitGTK-based desktop app." + + - name: Package build (Windows) + if: matrix.platform == 'win32' + shell: pwsh + run: | + Compress-Archive -Path publish/win32/* -DestinationPath CommandRunner-win-x64.zip + + - name: Package build (macOS) + if: matrix.platform == 'darwin' + run: | + cd publish/darwin + zip -r ../../CommandRunner-osx-x64.zip . + cd ../.. + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: command-runner-desktop-${{ matrix.platform }}-${{ github.run_number }} + path: | + CommandRunner-*.tar.gz + CommandRunner-*.zip + retention-days: 30 + + - name: Create Pre-release (on PR) + if: github.event_name == 'pull_request' + uses: softprops/action-gh-release@v1 + with: + tag_name: pr-${{ github.event.pull_request.number }}-photino-${{ matrix.platform }}-${{ github.run_number }} + name: PR #${{ github.event.pull_request.number }} Photino pre-release (${{ matrix.platform }}) + target_commitish: ${{ github.sha }} + files: | + CommandRunner-*.tar.gz + CommandRunner-*.zip + generate_release_notes: false + draft: false + prerelease: true + overwrite_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Release (on tag push) + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v1 + with: + files: | + CommandRunner-*.tar.gz + CommandRunner-*.zip + generate_release_notes: true + draft: false + prerelease: false + overwrite_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cleanup-workflow-run-logs: + needs: build-photino + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Cleanup workflow run logs + uses: igorjs/gh-actions-clean-workflow@v4 + with: + days_old: "7" + runs_to_keep: "5" diff --git a/.github/workflows/reusable-build-and-unit-test.yml b/.github/workflows/reusable-build-and-unit-test.yml index 0a4b10a..e652cd3 100644 --- a/.github/workflows/reusable-build-and-unit-test.yml +++ b/.github/workflows/reusable-build-and-unit-test.yml @@ -39,23 +39,6 @@ jobs: with: dotnet-version: ${{ matrix.dotnet-version }} - - name: Setup Node.js for React app - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: src/CommandRunner.ReactWebsite/package-lock.json - - - name: Install React dependencies - run: | - cd src/CommandRunner.ReactWebsite - npm ci - - - name: Build React app - run: | - cd src/CommandRunner.ReactWebsite - npm run build - - name: Restore .NET dependencies run: dotnet restore diff --git a/.gitignore b/.gitignore index 62b740f..c59990c 100644 --- a/.gitignore +++ b/.gitignore @@ -306,7 +306,7 @@ node_modules/ # Build outputs dist/ -dist-electron/ +publish/ *.AppImage *.deb *.rpm diff --git a/.vscode/launch.json b/.vscode/launch.json index 659e521..1d7d62d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,14 +1,5 @@ { "version": "0.2.0", - "compounds": [ - { - "name": "Command Runner (Electron + API)", - "configurations": [ - "Command Runner API", - "Command Runner Electron" - ] - } - ], "configurations": [ { "name": "Command Runner API", @@ -24,19 +15,18 @@ "console": "integratedTerminal" }, { - "name": "Command Runner Electron", - "type": "node", + "name": "Command Runner Desktop (Photino)", + "type": "coreclr", "request": "launch", - "runtimeExecutable": "npm", - "runtimeArgs": [ + "program": "dotnet", + "args": [ "run", - "electron-dev" + "--project", + "${workspaceFolder}/src/CommandRunner.Desktop/CommandRunner.Desktop.csproj" ], - "cwd": "${workspaceFolder}/src/CommandRunner.ReactWebsite", + "cwd": "${workspaceFolder}", "console": "integratedTerminal", - "skipFiles": [ - "/**" - ] + "preLaunchTask": "command-runner:vue-dev" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bd29ec5..41d40ca 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -29,14 +29,26 @@ } }, { - "label": "command-runner:electron-dev", + "label": "command-runner:vue-dev", "type": "npm", - "script": "electron-dev", - "path": "src/CommandRunner.ReactWebsite", - "dependsOn": [ - "command-runner:api" - ], - "problemMatcher": [], + "script": "dev", + "path": "src/CommandRunner.Website.VueJs", + "isBackground": true, + "problemMatcher": { + "owner": "command-runner-vue", + "pattern": [ + { + "regexp": "^(.*)$", + "file": 1, + "message": 1 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^.*$", + "endsPattern": "ready in" + } + }, "presentation": { "reveal": "always", "panel": "dedicated", diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a416ae2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,96 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Command Runner is a cross-platform desktop app for executing shell commands through a UI, backed by an ASP.NET Core (.NET 10) API that does the actual process execution. The frontend/packaging track is **Vue + Photino** (`CommandRunner.Website.VueJs` + `CommandRunner.Desktop`) — a single process hosts both the API and the built Vue static files on one dynamically-assigned port (nothing to guess or collide with). See the Architecture section below for details. + +## Code Style + +Don't add comments unless the WHY is genuinely non-obvious (a hidden constraint, a workaround for a specific bug, something that would surprise a reader). Well-named identifiers should speak for themselves — don't add comments that restate what the code already says. + +## Common Commands + +### Backend (.NET, from repo root) +```bash +dotnet restore +dotnet build --configuration Release --no-restore +dotnet test --configuration Release --no-build --verbosity normal # all unit tests (NUnit) +dotnet test --filter "FullyQualifiedName~CommandExecutionServiceTests" # single test class +cd src/CommandRunner.Api && dotnet run # API only, listens on http://localhost:5085 (see launchSettings.json) +``` +Test project is `test/CommandRunner.UnitTests` (NUnit). It references `CommandRunner.Api` — tests exercise services directly (e.g. `CommandExecutionService`, `CommandValidationService` from `CommandRunner.Api.Features.Commands`), not HTTP endpoints. + +### Full app in development +Preferred: VS Code launch config **"Command Runner Desktop (Photino)"** (`.vscode/launch.json`), which runs the `command-runner:vue-dev` task (Vite dev server) then launches `CommandRunner.Desktop`, which loads that dev server in `DEBUG` builds — see the Photino desktop host section below. Equivalent manually: `npm run dev` in `src/CommandRunner.Website.VueJs` in one terminal, `dotnet run --project src/CommandRunner.Desktop` in another. + +CI (`.github/workflows/build-and-test.yml` → `reusable-build-and-unit-test.yml`) runs on Ubuntu/Windows/macOS: `dotnet build` + `dotnet test` for the whole solution, plus a shell-compatibility smoke check (bash/PowerShell/cmd) reflecting that command execution must work correctly across those three shells. + +### Vue frontend (from `src/CommandRunner.Website.VueJs/`) +```bash +npm install +npm run dev # Vite dev server (localhost:5174), talks to a separately-run CommandRunner.Api +npm run build # vue-tsc + vite build -> dist/ +npm run lint # eslint, max-warnings 0 +``` +No `VITE_API_BASE_URL` needed when served by `CommandRunner.Desktop` (same-origin, see below) — only set it when running the Vite dev server against a separately-running API. + +### Photino desktop host (from repo root) +```bash +cd src/CommandRunner.Website.VueJs && npm run build # must run first -- populates CommandRunner.Desktop's wwwroot +dotnet run --project src/CommandRunner.Desktop +``` +`CommandRunner.Desktop` is a thin `Microsoft.NET.Sdk.Web` project that hosts the real `CommandRunner.Api` controllers (via a `ProjectReference` + `AddApplicationPart`, not duplicated code) and serves the Vue build's static files from the same Kestrel instance, then opens a native OS webview window (via Photino.NET) pointed at it. In `DEBUG` builds it loads `http://localhost:5174` (the Vite dev server) instead, for hot reload. CI: `.github/workflows/photino-build.yml`, self-contained `dotnet publish` per OS (`win-x64`/`linux-x64`/`osx-x64`), zipped rather than built into a full installer (no WiX/NSIS/`.app` bundling yet). Linux/macOS targets need `libwebkit2gtk` installed (system dependency, not bundled); Windows needs the WebView2 Runtime (preinstalled on Windows 11 / delivered via Windows Update on Windows 10 — deliberately not bundling a Fixed-Version runtime). + +**Kestrel binds to a fixed port (`http://127.0.0.1:5081`) in `DEBUG`, an OS-assigned one (`http://127.0.0.1:0`) in `Release`** — this asymmetry is deliberate, not an oversight. In `DEBUG` the webview loads the Vite dev server, not this Kestrel instance, so the Vue app's `window.location.origin` is `localhost:5174` — API calls need somewhere fixed to be proxied to, which is what `vite.config.ts`'s `server.proxy['/api'] → http://127.0.0.1:5081` targets. An OS-assigned port here would be unreachable from the dev server (nothing to point the proxy at). In `Release`, Desktop serves the built Vue bundle itself, so it's same-origin and the OS-assigned port works fine (nothing to guess or collide with in packaged builds). If you ever see profiles fail to load / the app stuck on "Running..." when running the Photino dev flow, check this port pairing first — it's exactly what breaks if either side drifts from the other. + +**Linux: blank white window under virtualized/software-rendered GPUs (e.g. VirtualBox VMs).** WebKitGTK's DMA-BUF renderer silently fails to composite anything in that environment — no error, just a permanently white webview, even though Kestrel is serving everything correctly (confirm with `curl` against the printed `Command Runner API listening on ...` URL if this comes up again). The fix is `WEBKIT_DISABLE_DMABUF_RENDERER=1`, but WebKitGTK reads it from the process's environment at exec() time — setting it in-process via `Environment.SetEnvironmentVariable()` inside `Main()` is provably too late (confirmed empirically: the window stays blank). `Program.cs` instead re-execs itself as a child process with the variable set from the outset (`OperatingSystem.IsLinux()` guard, skipped if already set, handles both the self-contained apphost and `dotnet run`/framework-dependent launch). If this class of bug resurfaces, `xwd`/`_NET_WM_PID` (or any X11 screenshot tool) plus reading `/proc//environ` — noting the latter only reflects the environment at the process's *original* exec(), not later in-process `setenv()` calls — is how it was diagnosed; don't trust an in-process env var fix without actually screenshotting the rendered window. + +## Architecture + +### .NET solution (`src/`) +``` +CommandRunner.Api # ASP.NET Core, organized as vertical feature slices — owns models, repositories, services, controllers, and DTOs +CommandRunner.Desktop # Photino host: hosts Api's controllers + Vue's static build in one process +CommandRunner.Console # Separate console entry point +``` +There is no separate Data or Business project — that layered split was replaced with a single `CommandRunner.Api` project organized as vertical feature slices, so a feature's persistence model, repository, business services, controller, and DTOs all live together instead of being spread across technical-layer projects. + +**`CommandRunner.Api` has no top-level `Models`/`Repositories`/`Services`/`Controllers`/`DTOs` folders.** Each feature is a `Features//` folder, namespaced `CommandRunner.Api.Features.`, holding everything that feature needs end to end: +``` +Features/Profiles/ # Profile, Command (the entity nested inside a profile), IProfileRepository, ProfileRepository, + # ProfileDto, CommandDto, ProfilesController +Features/Commands/ # ICommandExecutionService/CommandExecutionService, ICommandValidationService/CommandValidationService, + # IIterationService/IterationService (+ IterationOptions), ISecurityService/SecurityService (+ SecuritySettings), + # CommandExecutionResult, IterationProgress (+ IterationItemResult), ValidationResult, + # CommandExecutionRequest, CommandExecutionResponse, IterationExecutionResponse, IterationItemResultDto, + # CommandsController (execute/execute-stream/execute-iterative/execute-iterative-stream/validate) +Features/Directories/ # FavoriteDirectory, IFavoriteDirectoryRepository, FavoriteDirectoryRepository, + # FavoriteDirectoryDto, DirectoriesController +Shared/ # BaseJsonRepository — genuinely cross-feature (both ProfileRepository and FavoriteDirectoryRepository derive from it) +ServiceCollectionExtensions.cs # stays at the project root — a composition root that touches every feature, not itself a feature +``` +Profiles and Directories are pure data-plus-DTO features with no business-service layer at all — `ProfilesController`/`DirectoriesController` only depend on their own feature's repository. `CommandsController` is the one controller with real business logic, and it also depends on `CommandRunner.Api.Features.Profiles` (for `IProfileRepository` and the nested `Command` type it looks up and executes) — a feature slice reaching into another feature slice is normal here; it's the same dependency a profile's commands always had, just expressed as a same-project cross-namespace reference instead of a cross-project one. + +DTOs are hand-mapped to/from each feature's own model types inside its controller (see `ProfilesController.MapToDto`/`MapFromDto`); there is no AutoMapper. When adding a new feature, create a `Features//` folder with everything it needs (skip the services entirely if it has no business logic, the way Profiles/Directories do) rather than reintroducing shared `Models`/`Repositories`/`Services`/`Controllers`/`DTOs` folders. Only add to `Shared/` when something is genuinely used by more than one feature — don't default new code there. `[Route("api/[controller]")]` derives the route from the controller class name regardless of namespace/folder, so routes are unaffected by this structure. + +`AddCommandRunnerServices()` (`CommandRunner.Api/ServiceCollectionExtensions.cs`) is the single place that registers the repositories/services into DI. Both `CommandRunner.Api/Program.cs` and `CommandRunner.Desktop/Program.cs` call it — add new services there, not inline in either `Program.cs`, so the two hosts can't drift out of sync. `CommandRunner.Desktop` references `CommandRunner.Api.csproj` only (for its controllers, static-file hosting, and `AddCommandRunnerServices()`) — there's nothing else left to reference now that Data/Business are gone. `test/CommandRunner.UnitTests` references `CommandRunner.Api.csproj` directly and tests feature services in isolation (constructing `CommandExecutionService`, `CommandValidationService`, etc. and calling them directly) rather than through HTTP. + +**Persistence**: `BaseJsonRepository` (`CommandRunner.Api/Shared/BaseJsonRepository.cs`) is a generic in-memory-cache-over-JSON-file store — no database. Data lives in the OS app-data folder (`%APPDATA%/CommandRunner`, `~/.config/CommandRunner`, `~/Library/Application Support/CommandRunner`), one JSON file per entity type (e.g. `profiles.json`). Repository instances are registered `Scoped` in DI but the underlying cache is per-file, reloaded when the file's mtime changes. + +**Command execution** (`CommandExecutionService`, in `Features/Commands/`): wraps `System.Diagnostics.Process`. Two execution modes — buffered (`ExecuteCommandAsync`) and streaming (`ExecuteCommandWithStreamingAsync`, used for SSE endpoints). When `Command.Shell` is set, the executable+arguments are wrapped and re-quoted for that shell (cmd/powershell/bash) rather than run directly — Windows and Unix take different quoting paths in `CreateProcess`. Every execution is preceded by a call into `ICommandValidationService`. + +**Validation vs. security are separate services**, both in `Features/Commands/`: `CommandValidationService` checks structural correctness (name/executable present, working directory exists and is writable, executable resolvable via PATH, env var name/value sanity). `SecurityService` is a distinct concern — blocked-command list, dangerous-character/path-traversal checks, and a confirmation-required check (`RequiresConfirmationAsync`) that `CommandsController` must honor via `request.UserConfirmed` before executing. Do not merge these two — they're intentionally separate services with different responsibilities, even though they now sit in the same feature folder. + +**Iteration** (`IterationService`, in `Features/Commands/`): recursively walks a directory tree (depth-limited, include/exclude glob-ish patterns) and re-runs a command once per matching subdirectory, reusing `ICommandExecutionService` per target. Supports skip-on-error vs. stop-on-first-failure semantics and parallelism limits. + +**Streaming endpoints**: `CommandsController` has `execute`/`execute-iterative` (buffered JSON response) and `execute-stream`/`execute-iterative-stream` (Server-Sent Events) variants of the same operations. SSE handlers manually write `event:`/`data:` frames and flush after each line — when touching these, keep both the buffered and streaming versions in sync since they duplicate the command/DTO-building logic. + +### Vue frontend (`src/CommandRunner.Website.VueJs/`) +- Pinia store (`src/stores/app.ts`) is the single global state store. Actions that mutate profiles/directories optimistically update local state *and* fire the corresponding API call inline inside the action (not in a separate effect/thunk), rolling back or alerting on failure. Follow this same pattern when adding new state mutations here rather than introducing a different data-fetching approach. +- `src/services/api/client.ts`'s `API_BASE_URL` defaults to `window.location.origin`, **not** a hardcoded port. This matters: `CommandRunner.Desktop` serves the API and this frontend from the same origin on an OS-assigned dynamic port, so a hardcoded fallback (e.g. `http://localhost:5081`) silently breaks the desktop app once built — it did during development of this feature. Only set `VITE_API_BASE_URL` when running the Vite dev server against a separately-hosted API. +- No component library — hand-rolled components with BEM class names, native ` - {!selectedProfile ? ( - Select a profile first - ) : selectedProfile.commands.length === 0 ? ( - No commands in this profile - ) : ( - selectedProfile.commands.map((command) => ( - - {command.name} - - )) - )} - - - ); -}; - -export const CommandSelector = React.memo(CommandSelectorComponent); \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/components/DirectorySelector.tsx b/src/CommandRunner.ReactWebsite/src/components/DirectorySelector.tsx deleted file mode 100644 index 467b61d..0000000 --- a/src/CommandRunner.ReactWebsite/src/components/DirectorySelector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { FormControl, InputLabel, Select, MenuItem, SelectChangeEvent } from '@mui/material'; -import { FavoriteDirectoryDto } from '../services/api'; - -interface DirectorySelectorProps { - directories: FavoriteDirectoryDto[]; - selectedDirectory: FavoriteDirectoryDto | null; - onDirectoryChange: (directory: FavoriteDirectoryDto | null) => void; - size?: 'small' | 'medium'; -} - -const DirectorySelectorComponent: React.FC = ({ - directories, - selectedDirectory, - onDirectoryChange, - size = 'medium' -}) => { - const handleChange = (event: SelectChangeEvent) => { - const directoryId = event.target.value; - const directory = directories.find(d => d.id === directoryId) || null; - onDirectoryChange(directory); - }; - - return ( - - Directory - - - ); -}; - -export const DirectorySelector = React.memo(DirectorySelectorComponent); \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/components/HeaderButtons.tsx b/src/CommandRunner.ReactWebsite/src/components/HeaderButtons.tsx deleted file mode 100644 index ac0029a..0000000 --- a/src/CommandRunner.ReactWebsite/src/components/HeaderButtons.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { IconButton } from '@mui/material'; -import { Settings, Brightness4, Brightness7, CenterFocusStrong } from '@mui/icons-material'; - -interface HeaderButtonsProps { - themeMode: 'light' | 'dark'; - onToggleFocusMode: () => void; - onToggleTheme: () => void; - onSettingsClick: () => void; -} - -const HeaderButtons: React.FC = ({ - themeMode, - onToggleFocusMode, - onToggleTheme, - onSettingsClick, -}) => { - return ( - <> - - - - - {themeMode === 'dark' ? : } - - - - - - ); -}; - -export default HeaderButtons; \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/components/OutputPanel.tsx b/src/CommandRunner.ReactWebsite/src/components/OutputPanel.tsx deleted file mode 100644 index 86612b0..0000000 --- a/src/CommandRunner.ReactWebsite/src/components/OutputPanel.tsx +++ /dev/null @@ -1,351 +0,0 @@ -import React, { useState } from 'react'; -import { - Paper, - Box, - Typography, - Button, - Stack, - Divider, - IconButton, - CircularProgress, - useTheme, -} from '@mui/material'; -import { - ExpandLess, - ExpandMore, - Clear, - Error, -} from '@mui/icons-material'; - -interface OutputPanelProps { - output: string; - isLoading: boolean; - focusMode: boolean; - onClear: () => void; - onCopy?: () => void; - onSave?: () => void; -} - -export const OutputPanel: React.FC = ({ - output, - isLoading, - focusMode, - onClear, - onCopy, - onSave -}) => { - const theme = useTheme(); - const [showErrorDetails, setShowErrorDetails] = useState(false); - - // Format output to prettify exceptions and errors - const formatOutput = (text: string) => { - if (!text) return { type: 'plain', content: text }; - - const lines = text.split('\n'); - - // Generic .NET exception detection - const isDotNetException = lines.some(line => - line.includes('System.') && line.includes('Exception') - ); - - if (isDotNetException) { - // Find the main exception message - const mainException = lines.find(line => - line.trim().startsWith('System.') && - line.includes('Exception:') && - !line.includes('--->') - ); - - const unhandledException = lines.find(line => - line.includes('Unhandled exception') || - line.includes('An unhandled exception') - ); - - let errorMessage = 'An error occurred'; - if (mainException) { - const colonIndex = mainException.indexOf('Exception:'); - if (colonIndex !== -1) { - errorMessage = mainException.substring(colonIndex + 11).trim(); - } - } else if (unhandledException) { - errorMessage = unhandledException.trim(); - } - - return { - type: 'error', - title: 'Application Error', - message: errorMessage, - suggestions: [ - 'Check if required services are running', - 'Verify configuration settings', - 'Ensure all dependencies are installed', - 'Check file and network permissions' - ], - fullDetails: text - }; - } - - // Command not found errors - if (lines.some(line => - line.includes('command not found') || - line.includes('is not recognized') || - line.includes('No such file or directory') - )) { - return { - type: 'error', - title: 'Command Not Found', - message: 'The specified command could not be located', - suggestions: [ - 'Install the required software', - 'Use absolute paths to executables', - 'Check if the command is in your PATH', - 'Verify the executable name is correct' - ], - fullDetails: text - }; - } - - // Permission errors - if (lines.some(line => - line.includes('Permission denied') || - line.includes('Access is denied') || - line.includes('Operation not permitted') - )) { - return { - type: 'error', - title: 'Permission Denied', - message: 'Access to the requested resource was denied', - suggestions: [ - 'Run with elevated privileges (sudo/admin)', - 'Check file ownership and permissions', - 'Verify access to the target directory', - 'Ensure the user has necessary rights' - ], - fullDetails: text - }; - } - - // Connection errors - if (lines.some(line => - line.includes('Connection refused') || - line.includes('Network is unreachable') || - line.includes('Address already in use') - )) { - return { - type: 'error', - title: 'Connection Error', - message: 'Network or connection-related error occurred', - suggestions: [ - 'Check if the target service is running', - 'Verify network connectivity', - 'Ensure the correct port/address is used', - 'Check firewall settings' - ], - fullDetails: text - }; - } - - // File system errors - if (lines.some(line => - line.includes('No such file') || - line.includes('Directory not found') || - line.includes('File exists') - )) { - return { - type: 'error', - title: 'File System Error', - message: 'File or directory operation failed', - suggestions: [ - 'Verify the file/directory path exists', - 'Check file permissions', - 'Use absolute paths instead of relative paths', - 'Ensure the working directory is correct' - ], - fullDetails: text - }; - } - - return { type: 'plain', content: text }; - }; - - // Component to render formatted output - const FormattedOutput = ({ formatted }: { formatted: any }) => { - if (formatted.type === 'plain') { - return {formatted.content}; - } - - if (formatted.type === 'error') { - return ( - - - - - {formatted.title} - - - - - {formatted.message} - - - - - 💡 Suggestions: - - - {formatted.suggestions.map((suggestion: string, index: number) => ( - - {suggestion} - - ))} - - - - - - - {showErrorDetails && ( - - - {formatted.fullDetails} - - - )} - - - ); - } - - return {String(formatted)}; - }; - - if (focusMode) { - // Collapsible output panel for focus mode - only show for actual errors - const formatted = formatOutput(output); - const hasError = formatted.type === 'error' && output && output !== 'Ready to execute commands...'; - - if (!hasError) return null; - - return ( - - - - ⚠️ Error Output - - - - - - - - - {isLoading ? ( - - - Loading... - - ) : } - - - ); - } - - // Full output window for normal mode - return ( - - - - Output Window - - - - {onCopy && ( - - )} - {onSave && ( - - )} - - - - - {isLoading && ( - - - Running... - - )} - {output ? : 'Ready to execute commands...'} - - - ); -}; diff --git a/src/CommandRunner.ReactWebsite/src/components/ProfileSelector.tsx b/src/CommandRunner.ReactWebsite/src/components/ProfileSelector.tsx deleted file mode 100644 index b2429be..0000000 --- a/src/CommandRunner.ReactWebsite/src/components/ProfileSelector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { FormControl, InputLabel, Select, MenuItem, SelectChangeEvent } from '@mui/material'; -import { ProfileDto } from '../services/api'; - -interface ProfileSelectorProps { - profiles: ProfileDto[]; - selectedProfile: ProfileDto | null; - onProfileChange: (profile: ProfileDto | null) => void; - size?: 'small' | 'medium'; -} - -const ProfileSelectorComponent: React.FC = ({ - profiles, - selectedProfile, - onProfileChange, - size = 'medium' -}) => { - const handleChange = (event: SelectChangeEvent) => { - const profileId = event.target.value; - const profile = profiles.find(p => p.id === profileId) || null; - onProfileChange(profile); - }; - - return ( - - Profile - - - ); -}; - -export const ProfileSelector = React.memo(ProfileSelectorComponent); \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/components/SettingsDialog.tsx b/src/CommandRunner.ReactWebsite/src/components/SettingsDialog.tsx deleted file mode 100644 index 194fcfd..0000000 --- a/src/CommandRunner.ReactWebsite/src/components/SettingsDialog.tsx +++ /dev/null @@ -1,700 +0,0 @@ -import { useState, useEffect, useRef, type ChangeEvent, type ReactNode } from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Box, - IconButton, - TextField, - FormControl, - InputLabel, - Select, - MenuItem, - Switch, - FormControlLabel, - Divider, - Typography, - Fab, - Card, - CardContent, - CardActions, - Chip, -} from '@mui/material'; -import { - Add, - Delete, - UploadFile, - DragIndicator, - Settings as SettingsIcon, -} from '@mui/icons-material'; -import { - DndContext, - PointerSensor, - closestCenter, - useSensor, - useSensors, - type DragEndEvent, -} from '@dnd-kit/core'; -import { - SortableContext, - useSortable, - verticalListSortingStrategy, - arrayMove, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { useApp } from '../contexts/AppContext'; -import { ProfileDto, CommandDto, profilesApi } from '../services/api'; - -interface SettingsDialogProps { - open: boolean; - onClose: () => void; -} - -interface SortableCardProps { - id: string; - children: ReactNode; -} - -function SortableCard({ id, children }: SortableCardProps) { - const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); - - return ( - - - - - - - {children} - - ); -} - -export function SettingsDialog({ open, onClose }: SettingsDialogProps) { - const { state, dispatch } = useApp(); - const sensors = useSensors(useSensor(PointerSensor)); - const profileFileInputRef = useRef(null); - const [showProfileForm, setShowProfileForm] = useState(false); - const [showCommandForm, setShowCommandForm] = useState(false); - const [editingItem, setEditingItem] = useState(null); - const [profileForm, setProfileForm] = useState({ name: '', description: '' }); - const [commandForm, setCommandForm] = useState>({ - name: '', - executable: '', - arguments: '', - workingDirectory: '', - shell: 'bash', - environmentVariables: {}, - iterationEnabled: false, - requireConfirmation: false, - }); - const [isSubmitting, setIsSubmitting] = useState(false); // Local state for immediate feedback - - const handleProfilesDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) { - return; - } - - const oldIndex = state.profiles.findIndex((profile) => profile.id === active.id); - const newIndex = state.profiles.findIndex((profile) => profile.id === over.id); - - if (oldIndex === -1 || newIndex === -1) { - return; - } - - const reorderedProfiles = arrayMove(state.profiles, oldIndex, newIndex); - dispatch({ type: 'SET_PROFILES', payload: reorderedProfiles }); - }; - - const handleCommandsDragEnd = (event: DragEndEvent) => { - if (!state.selectedProfile) { - return; - } - - const { active, over } = event; - if (!over || active.id === over.id) { - return; - } - - const oldIndex = state.selectedProfile.commands.findIndex((command) => command.id === active.id); - const newIndex = state.selectedProfile.commands.findIndex((command) => command.id === over.id); - - if (oldIndex === -1 || newIndex === -1) { - return; - } - - const reorderedCommands = arrayMove(state.selectedProfile.commands, oldIndex, newIndex); - const updatedProfile = { - ...state.selectedProfile, - commands: reorderedCommands, - updatedAt: new Date().toISOString(), - }; - - dispatch({ type: 'UPDATE_PROFILE', payload: updatedProfile }); - }; - - const handleImportProfile = async (event: ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) { - return; - } - - try { - const importedProfile = await profilesApi.importProfile(file); - dispatch({ type: 'SET_PROFILES', payload: [...state.profiles, importedProfile] }); - dispatch({ type: 'SET_SELECTED_PROFILE', payload: importedProfile }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to import profile'; - alert(message); - } finally { - event.target.value = ''; - } - }; - - // Reset state when dialog opens - useEffect(() => { - if (open) { - setShowProfileForm(false); - setShowCommandForm(false); - setEditingItem(null); - setProfileForm({ name: '', description: '' }); - setCommandForm({ - name: '', - executable: '', - arguments: '', - workingDirectory: '', - shell: 'bash', - environmentVariables: {}, - iterationEnabled: false, - requireConfirmation: false, - }); - } - }, [open]); - - // Reset creatingProfile flag after profile operations - useEffect(() => { - if (state.creatingProfile) { - // Reset the flag after 3 seconds to allow API call to complete - const timeout = setTimeout(() => { - console.log('🔄 Auto-resetting creatingProfile flag'); - dispatch({ type: 'SET_CREATING_PROFILE', payload: false }); - }, 3000); - - return () => clearTimeout(timeout); - } - }, [state.creatingProfile, dispatch]); - - // Also reset when profiles change (indicating successful creation) - useEffect(() => { - if (state.creatingProfile && state.profiles.length > 0) { - // If we were creating a profile and now have profiles, reset the flag - const timeout = setTimeout(() => { - console.log('🔄 Resetting creatingProfile flag after profile list update'); - dispatch({ type: 'SET_CREATING_PROFILE', payload: false }); - }, 500); - - return () => clearTimeout(timeout); - } - }, [state.profiles.length, state.creatingProfile, dispatch]); - - const handleAddProfile = () => { - setShowProfileForm(true); - setEditingItem(null); - setProfileForm({ name: '', description: '' }); - }; - - const handleEditProfile = (profile: ProfileDto) => { - dispatch({ type: 'SET_SELECTED_PROFILE', payload: profile }); - setShowProfileForm(false); - setShowCommandForm(false); - setEditingItem(null); - }; - - const handleEditProfileDetails = (profile: ProfileDto) => { - setProfileForm({ name: profile.name, description: profile.description || '' }); - setEditingItem(profile); - setShowProfileForm(true); - setShowCommandForm(false); - }; - - const handleSaveProfile = () => { - if (!profileForm.name.trim()) { - alert('Please enter a profile name'); - return; - } - - // Prevent duplicate profile creation requests using local state - if (isSubmitting || state.creatingProfile) { - console.warn('🚫 Profile creation already in progress'); - return; - } - - setIsSubmitting(true); // Set local state immediately - - const trimmedName = profileForm.name.trim(); - console.log('📝 Creating profile with name:', `"${trimmedName}"`); - console.log('📝 Original name:', `"${profileForm.name}"`); - console.log('📝 Name length:', trimmedName.length); - - const profileData: ProfileDto = { - id: editingItem?.id || `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - name: trimmedName, - description: profileForm.description.trim(), - commands: editingItem?.commands || [], - createdAt: editingItem?.createdAt || new Date().toISOString(), - updatedAt: new Date().toISOString(), - isFavorite: editingItem?.isFavorite || false, - }; - - console.log('📝 Final profile data:', profileData); - - if (editingItem) { - dispatch({ type: 'UPDATE_PROFILE', payload: profileData }); - // Reset local state immediately for updates - setIsSubmitting(false); - } else { - dispatch({ type: 'ADD_PROFILE', payload: profileData }); - // Reset local state after a delay for new profiles (to allow API call to complete) - setTimeout(() => { - setIsSubmitting(false); - // Refresh profiles to ensure we have the latest data from server - console.log('🔄 Refreshing profiles after creation...'); - // The profiles will be refreshed when the API call completes - }, 3000); - } - - setShowProfileForm(false); - setEditingItem(null); - setProfileForm({ name: '', description: '' }); - }; - - const handleDeleteProfile = (profile: ProfileDto) => { - if (!profile.id || profile.id.trim() === '') { - alert('Cannot delete profile: Invalid profile ID'); - return; - } - - if (confirm(`Delete profile "${profile.name}" and all its commands?`)) { - dispatch({ type: 'DELETE_PROFILE', payload: profile.id }); - } - }; - - const handleAddCommand = () => { - setShowCommandForm(true); - setEditingItem(null); - setCommandForm({ - name: '', - executable: '', - arguments: '', - workingDirectory: state.selectedProfile?.commands[0]?.workingDirectory || '~/', - shell: 'bash', - environmentVariables: {}, - iterationEnabled: false, - requireConfirmation: false, - }); - }; - - const handleEditCommand = (command: CommandDto) => { - setEditingItem(command); - setCommandForm({ ...command }); - setShowCommandForm(true); - }; - - const handleSaveCommand = () => { - if (!state.selectedProfile || !commandForm.name || !commandForm.executable) { - alert('Please fill in Name and Executable'); - return; - } - - const commandData: CommandDto = { - id: editingItem?.id || `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - name: commandForm.name, - executable: commandForm.executable, - arguments: commandForm.arguments || '', - workingDirectory: commandForm.workingDirectory || '~/', - shell: commandForm.shell || 'bash', - environmentVariables: commandForm.environmentVariables || {}, - iterationEnabled: commandForm.iterationEnabled || false, - requireConfirmation: commandForm.requireConfirmation || false, - createdAt: editingItem?.createdAt || new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const updatedProfile = { - ...state.selectedProfile, - commands: editingItem - ? state.selectedProfile.commands.map(cmd => - cmd.id === editingItem.id ? commandData : cmd - ) - : [...state.selectedProfile.commands, commandData], - }; - - dispatch({ type: 'UPDATE_PROFILE', payload: updatedProfile }); - setShowCommandForm(false); - setEditingItem(null); - }; - - const handleDeleteCommand = (command: CommandDto) => { - if (!state.selectedProfile) return; - - if (confirm(`Delete command "${command.name}"?`)) { - const updatedProfile = { - ...state.selectedProfile, - commands: state.selectedProfile.commands.filter(cmd => cmd.id !== command.id), - }; - dispatch({ type: 'UPDATE_PROFILE', payload: updatedProfile }); - } - }; - - return ( - - - - Manage Profiles & Commands - - - - {!showProfileForm && !showCommandForm ? ( - // Main view - Profiles grid - - - Profiles - - - - - - - - - - profile.id)} strategy={verticalListSortingStrategy}> - - {state.profiles.map((profile) => ( - - { - if (!(e.target as HTMLElement).closest('button')) { - handleEditProfile(profile); - } - }} - > - - - - - {profile.name} - - - {profile.description || 'No description'} - - - - {profile.isFavorite && ( - - )} - - - - - - - { - e.stopPropagation(); - handleDeleteProfile(profile); - }} - title="Delete this profile and all its commands" - > - - - - - - ))} - - - - - {state.selectedProfile && ( - - - - - Commands in "{state.selectedProfile.name}" ({state.selectedProfile.commands.length}) - - - - - - - {state.selectedProfile.commands.length === 0 ? ( - - - No commands in this profile yet - - - - ) : ( - - command.id)} - strategy={verticalListSortingStrategy} - > - - {state.selectedProfile.commands.map((command) => ( - - - - - {command.name} - - theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100', p: 1, borderRadius: 1, mb: 1 }}> - {command.executable} {command.arguments} - - - 📁 {command.workingDirectory} - - - {command.iterationEnabled && ( - - )} - {command.requireConfirmation && ( - - )} - {command.shell && command.shell !== 'bash' && ( - - )} - {Object.keys(command.environmentVariables || {}).length > 0 && ( - - )} - - Created: {new Date(command.createdAt).toLocaleDateString()} - - - - - - handleDeleteCommand(command)} - title="Delete this command" - > - - - - - - ))} - - - - )} - - )} - - ) : showProfileForm ? ( - // Profile form - - - {editingItem ? 'Edit Profile' : 'Add New Profile'} - - - setProfileForm({ ...profileForm, name: e.target.value })} - required - autoFocus - variant="filled" - /> - setProfileForm({ ...profileForm, description: e.target.value })} - multiline - rows={2} - variant="filled" - /> - - - - - - - ) : ( - // Command form - - - {editingItem ? 'Edit Command' : 'Add New Command'} - - - setCommandForm({ ...commandForm, name: e.target.value })} - required - autoFocus - variant="filled" - /> - setCommandForm({ ...commandForm, executable: e.target.value })} - required - placeholder="e.g., git, npm, python" - variant="filled" - /> - setCommandForm({ ...commandForm, arguments: e.target.value })} - placeholder="e.g., --version, pull --rebase" - variant="filled" - /> - setCommandForm({ ...commandForm, workingDirectory: e.target.value })} - placeholder="e.g., ~/projects, /usr/local/bin" - variant="filled" - /> - - Shell - - - - setCommandForm({ ...commandForm, iterationEnabled: e.target.checked })} - /> - } - label="Run in subdirectories" - /> - setCommandForm({ ...commandForm, requireConfirmation: e.target.checked })} - /> - } - label="Require confirmation" - /> - - - - - - - - )} - - - - - - - - ); -} diff --git a/src/CommandRunner.ReactWebsite/src/contexts/AppContext.tsx b/src/CommandRunner.ReactWebsite/src/contexts/AppContext.tsx deleted file mode 100644 index 83a4e59..0000000 --- a/src/CommandRunner.ReactWebsite/src/contexts/AppContext.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react'; -import { ProfileDto, FavoriteDirectoryDto, CommandExecutionResponse } from '../services/api'; -import { profilesApi, directoriesApi } from '../services/api'; - -// State interface -interface AppState { - profiles: ProfileDto[]; - directories: FavoriteDirectoryDto[]; - selectedProfile: ProfileDto | null; - selectedDirectory: FavoriteDirectoryDto | null; - isLoading: boolean; - creatingProfile: boolean; // Prevent duplicate profile creation - error: string | null; - executionHistory: CommandExecutionResponse[]; - theme: 'light' | 'dark'; -} - -// Action types -type AppAction = - | { type: 'SET_LOADING'; payload: boolean } - | { type: 'SET_ERROR'; payload: string | null } - | { type: 'SET_PROFILES'; payload: ProfileDto[] } - | { type: 'ADD_PROFILE'; payload: ProfileDto } - | { type: 'UPDATE_PROFILE'; payload: ProfileDto } - | { type: 'DELETE_PROFILE'; payload: string } - | { type: 'SET_SELECTED_PROFILE'; payload: ProfileDto | null } - | { type: 'SET_DIRECTORIES'; payload: FavoriteDirectoryDto[] } - | { type: 'ADD_DIRECTORY'; payload: FavoriteDirectoryDto } - | { type: 'UPDATE_DIRECTORY'; payload: FavoriteDirectoryDto } - | { type: 'DELETE_DIRECTORY'; payload: string } - | { type: 'SET_SELECTED_DIRECTORY'; payload: FavoriteDirectoryDto | null } - | { type: 'ADD_EXECUTION'; payload: CommandExecutionResponse } - | { type: 'CLEAR_EXECUTION_HISTORY' } - | { type: 'SET_THEME'; payload: 'light' | 'dark' } - | { type: 'SET_CREATING_PROFILE'; payload: boolean }; - -// Initial state -const initialState: AppState = { - profiles: [], // Start with empty arrays to force API loading - directories: [], - selectedProfile: null, - selectedDirectory: null, - isLoading: false, - creatingProfile: false, - error: null, - executionHistory: [], - theme: 'dark', -}; - -// Reducer -function appReducer(state: AppState, action: AppAction): AppState { - switch (action.type) { - case 'SET_LOADING': - return { ...state, isLoading: action.payload }; - case 'SET_ERROR': - return { ...state, error: action.payload }; - case 'SET_PROFILES': - return { ...state, profiles: action.payload }; - case 'ADD_PROFILE': - console.log('📝 Adding profile to local state:', action.payload.name, action.payload.id); - - // Prevent duplicate requests by checking if we're already creating a profile - if (state.creatingProfile) { - console.warn('🚫 Profile creation already in progress, ignoring duplicate request'); - return state; - } - - // Try to save to API - profilesApi.createProfile(action.payload).then((createdProfile) => { - console.log('✅ Successfully saved profile to API:', createdProfile.name, createdProfile.id); - // Profile created successfully, keep it in local state - // The creatingProfile flag will be reset by the useEffect in SettingsDialog - }).catch(error => { - console.error('❌ Failed to save profile to API:', error); - // Show user-friendly error message - const errorMessage = error.message || 'Unknown error occurred'; - console.error('❌ Profile creation error details:', errorMessage); - - // Remove the failed profile from local state - console.log('🗑️ Removing failed profile from local state:', action.payload.id); - // The creatingProfile flag will be reset by the useEffect in SettingsDialog - }); - - return { - ...state, - profiles: [...state.profiles, action.payload], - creatingProfile: true - }; - case 'UPDATE_PROFILE': - // Try to save to API, but don't block the UI - profilesApi.updateProfile(action.payload.id, action.payload).catch(error => { - console.warn('Failed to update profile via API:', error); - }); - return { - ...state, - profiles: state.profiles.map(p => p.id === action.payload.id ? action.payload : p), - selectedProfile: state.selectedProfile?.id === action.payload.id ? action.payload : state.selectedProfile - }; - case 'DELETE_PROFILE': - // Delete from local state immediately for better UX - const updatedProfiles = state.profiles.filter(p => p.id !== action.payload); - const newState = { - ...state, - profiles: updatedProfiles, - selectedProfile: state.selectedProfile?.id === action.payload ? null : state.selectedProfile - }; - - // Try to delete from API (don't block UI) - profilesApi.deleteProfile(action.payload).then(() => { - console.log('Successfully deleted profile from API:', action.payload); - }).catch(error => { - console.error('Failed to delete profile from API:', error); - console.error('Error details:', error.response?.data || error.message); - - // If API deletion fails, show error to user and restore the profile - const deletedProfile = state.profiles.find(p => p.id === action.payload); - if (deletedProfile) { - setTimeout(() => { - alert(`Failed to delete profile from server. It will reappear on refresh.\nError: ${error.message || 'Unknown error'}`); - // Optionally restore the profile to local state - // dispatch({ type: 'ADD_PROFILE', payload: deletedProfile }); - }, 100); - } - }); - - return newState; - case 'SET_SELECTED_PROFILE': - return { ...state, selectedProfile: action.payload }; - case 'SET_DIRECTORIES': - return { ...state, directories: action.payload }; - case 'ADD_DIRECTORY': - return { ...state, directories: [...state.directories, action.payload] }; - case 'UPDATE_DIRECTORY': - return { - ...state, - directories: state.directories.map(d => d.id === action.payload.id ? action.payload : d), - selectedDirectory: state.selectedDirectory?.id === action.payload.id ? action.payload : state.selectedDirectory - }; - case 'DELETE_DIRECTORY': - return { - ...state, - directories: state.directories.filter(d => d.id !== action.payload), - selectedDirectory: state.selectedDirectory?.id === action.payload ? null : state.selectedDirectory - }; - case 'SET_SELECTED_DIRECTORY': - return { ...state, selectedDirectory: action.payload }; - case 'ADD_EXECUTION': - return { ...state, executionHistory: [action.payload, ...state.executionHistory].slice(0, 50) }; // Keep last 50 - case 'CLEAR_EXECUTION_HISTORY': - return { ...state, executionHistory: [] }; - case 'SET_THEME': - return { ...state, theme: action.payload }; - case 'SET_CREATING_PROFILE': - return { ...state, creatingProfile: action.payload }; - default: - return state; - } -} - -// Context -const AppContext = createContext<{ - state: AppState; - dispatch: React.Dispatch; -} | null>(null); - -// Provider component -export function AppProvider({ children }: { children: ReactNode }) { - const [state, dispatch] = useReducer(appReducer, initialState); - - // Load theme from localStorage on startup - useEffect(() => { - const savedTheme = localStorage.getItem('theme'); - if (savedTheme === 'light' || savedTheme === 'dark') { - dispatch({ type: 'SET_THEME', payload: savedTheme }); - } else { - // Default to dark if not set - dispatch({ type: 'SET_THEME', payload: 'dark' }); - } - }, []); - - // Load initial data - useEffect(() => { - const loadInitialData = async () => { - console.log('🚀 Loading initial data from API...'); - dispatch({ type: 'SET_LOADING', payload: true }); - - // Clear any cached data first - console.log('🧹 Clearing localStorage cache...'); - localStorage.removeItem('commandRunner_profiles'); - localStorage.removeItem('commandRunner_directories'); - - try { - // Try to load from API first - console.log('📡 Calling API endpoints...'); - const [profiles, directories] = await Promise.all([ - profilesApi.getAllProfiles(), - directoriesApi.getAllDirectories() - ]); - - console.log('📊 API Response - Profiles:', profiles.length, 'Directories:', directories.length); - console.log('📊 Directories data:', directories); - - // Update with API data (even if empty arrays) - const profilesWithIds = profiles.map(profile => ({ - ...profile, - id: profile.id || `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` - })); - console.log('✅ Loading profiles from API:', profilesWithIds.length); - dispatch({ type: 'SET_PROFILES', payload: profilesWithIds }); - - console.log('✅ Loading directories from API:', directories.length); - dispatch({ type: 'SET_DIRECTORIES', payload: directories }); - - } catch (error) { - // API connection failed - provide detailed error information - console.warn('❌ API not available:', error); - - // Extract meaningful error information - let errorMessage = 'Unable to connect to Command Runner API'; - let errorDetails = ''; - - const err = error as any; // Type assertion for error handling - - if (err.code === 'ECONNREFUSED') { - errorMessage = 'API server is not running'; - errorDetails = 'Please ensure the Command Runner API is started. In development, run the API project. In production, the API should start automatically.'; - } else if (err.code === 'ENOTFOUND') { - errorMessage = 'API server not found'; - errorDetails = 'The API server may not be running or the connection URL is incorrect.'; - } else if (err.response) { - errorMessage = `API server error (${err.response.status})`; - errorDetails = err.response.data?.message || err.response.statusText || 'Unknown server error'; - } else if (err.request) { - errorMessage = 'Network connection failed'; - errorDetails = 'Please check your internet connection and ensure the API server is accessible.'; - } - - const fullErrorMessage = errorDetails - ? `${errorMessage}\n\n${errorDetails}` - : errorMessage; - - console.log('⚠️ API failed, keeping empty data'); - dispatch({ type: 'SET_PROFILES', payload: [] }); - dispatch({ type: 'SET_DIRECTORIES', payload: [] }); - - dispatch({ type: 'SET_ERROR', payload: fullErrorMessage }); - } finally { - dispatch({ type: 'SET_LOADING', payload: false }); - } - }; - - loadInitialData(); - }, []); - - // Save theme to localStorage - useEffect(() => { - localStorage.setItem('theme', state.theme); - }, [state.theme]); - - // Save profiles to localStorage - useEffect(() => { - localStorage.setItem('commandRunner_profiles', JSON.stringify(state.profiles)); - }, [state.profiles]); - - // Save directories to localStorage - useEffect(() => { - localStorage.setItem('commandRunner_directories', JSON.stringify(state.directories)); - }, [state.directories]); - - return ( - - {children} - - ); -} - -// Hook to use the context -export function useApp() { - const context = useContext(AppContext); - if (!context) { - throw new Error('useApp must be used within an AppProvider'); - } - return context; -} \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/index.css b/src/CommandRunner.ReactWebsite/src/index.css deleted file mode 100644 index 9817e40..0000000 --- a/src/CommandRunner.ReactWebsite/src/index.css +++ /dev/null @@ -1,69 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/main.tsx b/src/CommandRunner.ReactWebsite/src/main.tsx deleted file mode 100644 index c7237ef..0000000 --- a/src/CommandRunner.ReactWebsite/src/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import { AppProvider } from './contexts/AppContext.tsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - , -) \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/services/api/client.ts b/src/CommandRunner.ReactWebsite/src/services/api/client.ts deleted file mode 100644 index 337914d..0000000 --- a/src/CommandRunner.ReactWebsite/src/services/api/client.ts +++ /dev/null @@ -1,133 +0,0 @@ -import axios, { AxiosInstance, AxiosResponse } from 'axios'; - -// Base API configuration -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5081'; - -console.log('API Base URL:', API_BASE_URL); - -class ApiClient { - private client: AxiosInstance; - - constructor() { - this.client = axios.create({ - baseURL: API_BASE_URL, - timeout: 30000, // 30 seconds - headers: { - 'Content-Type': 'application/json', - }, - }); - - // Request interceptor for logging - this.client.interceptors.request.use( - (config) => { - console.log('🔄 API Request:', config.method?.toUpperCase(), config.url); - console.log('📤 Request Data:', config.data); - console.log('🔗 Full URL:', `${config.baseURL || 'undefined'}${config.url || ''}`); - // Check for duplicate requests - if (config.method?.toUpperCase() === 'POST' && config.url?.includes('/profiles')) { - console.log('🚨 PROFILE CREATION REQUEST - Checking for duplicates'); - const profileData = config.data || {}; - console.log('🚨 Profile name being sent:', profileData.name); - console.log('🚨 Full profile data:', profileData); - } - return config; - }, - (error) => { - console.error('❌ API Request Error:', error); - console.error('❌ Request Config:', error.config); - return Promise.reject(error); - } - ); - - // Response interceptor for error handling - this.client.interceptors.response.use( - (response: AxiosResponse) => { - console.log('✅ API Response:', response.status, response.config.method?.toUpperCase(), response.config.url); - console.log('📥 Response Data:', response.data); - console.log('📥 Raw Response:', response); - return response; - }, - (error) => { - console.error('❌ API Response Error:', error); - console.error('❌ Full Error Object:', error); - if (error.response) { - console.error('❌ Error Response Status:', error.response.status); - console.error('❌ Error Response Headers:', error.response.headers); - console.error('❌ Error Response Data (raw):', error.response.data); - console.error('❌ Error Response Data Type:', typeof error.response.data); - - // Server responded with error status - let message = 'API Error'; - - // Try to extract the specific error message from different possible formats - if (error.response.data) { - if (typeof error.response.data === 'string') { - console.error('❌ Response is string, first 200 chars:', error.response.data.substring(0, 200)); - message = error.response.data; - } else if (error.response.data.message) { - message = error.response.data.message; - } else if (error.response.data.error) { - message = error.response.data.error; - } else if (error.response.data.title) { - message = error.response.data.title; - } - } - - // Include status code for context - const statusCode = error.response.status; - console.error(`❌ HTTP ${statusCode} Error Details:`, error.response.data); - if (statusCode === 409) { - message = `Conflict: ${message}`; - } else if (statusCode === 400) { - message = `Bad Request: ${message}`; - } else if (statusCode === 401) { - message = `Unauthorized: ${message}`; - } else if (statusCode === 403) { - message = `Forbidden: ${message}`; - } else if (statusCode === 404) { - message = `Not Found: ${message}`; - } else if (statusCode >= 500) { - message = `Server Error: ${message}`; - } - - throw new Error(message); - } else if (error.request) { - // Network error - console.error('❌ Network Error - Request made but no response received'); - console.error('❌ Request details:', error.request); - throw new Error('Network error - please check your connection'); - } else { - // Other error - console.error('❌ Unknown API Error:', error.message); - throw new Error(error.message || 'Unknown API error'); - } - } - ); - } - - // Generic GET request - async get(url: string, params?: any): Promise { - const response = await this.client.get(url, { params }); - return response.data; - } - - // Generic POST request - async post(url: string, data?: any): Promise { - const response = await this.client.post(url, data); - return response.data; - } - - // Generic PUT request - async put(url: string, data?: any): Promise { - const response = await this.client.put(url, data); - return response.data; - } - - // Generic DELETE request - async delete(url: string): Promise { - const response = await this.client.delete(url); - return response.data; - } -} - -export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/services/api/commands.ts b/src/CommandRunner.ReactWebsite/src/services/api/commands.ts deleted file mode 100644 index 538d2c8..0000000 --- a/src/CommandRunner.ReactWebsite/src/services/api/commands.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { apiClient } from './client'; -import { - CommandExecutionRequest, - CommandExecutionResponse, - IterationExecutionResponse, - ValidationResult -} from './types'; - -type StreamingHandlers = { - onStdout?: (line: string) => void; - onStderr?: (line: string) => void; - onError?: (message: string) => void; -}; - -type IterativeStreamingHandlers = { - onItemStart?: (itemPath: string, itemName: string) => void; - onStdout?: (itemPath: string, line: string) => void; - onStderr?: (itemPath: string, line: string) => void; - onProgress?: (progress: { - totalItems: number; - processedItems: number; - successfulItems: number; - failedItems: number; - skippedItems: number; - currentItem: string; - currentDirectory: string; - isCompleted: boolean; - wasCancelled: boolean; - startedAt: string; - completedAt?: string; - }) => void; - onItemComplete?: (item: { - itemPath: string; - wasSuccessful: boolean; - errorMessage?: string; - output: string; - errorOutput: string; - executionTime?: string; - executedAt: string; - }) => void; - onError?: (message: string) => void; -}; - -export class CommandsApiService { - // Execute a single command - async executeCommand(request: CommandExecutionRequest): Promise { - return apiClient.post('/api/commands/execute', request); - } - - // Execute command iteratively (in multiple directories) - async executeIterativeCommand(request: CommandExecutionRequest): Promise { - return apiClient.post('/api/commands/execute-iterative', request); - } - - // Validate command security - async validateCommand(profileId: string, commandId: string): Promise { - return apiClient.post(`/api/commands/validate/${profileId}/${commandId}`); - } - - - // Stream execution output via Server-Sent Events - async executeCommandWithStreaming( - request: CommandExecutionRequest, - handlers?: StreamingHandlers, - signal?: AbortSignal - ): Promise { - const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5081'; - const response = await fetch(`${apiBaseUrl}/api/commands/execute-stream`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'text/event-stream' - }, - body: JSON.stringify(request), - signal - }); - - if (!response.ok || !response.body) { - throw new Error(`Failed to start streaming command execution (${response.status})`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let completedResponse: CommandExecutionResponse | null = null; - - const parseEvent = (rawEvent: string) => { - const lines = rawEvent.split('\n'); - let eventName = 'message'; - const dataLines: string[] = []; - - for (const line of lines) { - if (line.startsWith('event:')) { - eventName = line.slice(6).trim(); - } - - if (line.startsWith('data:')) { - dataLines.push(line.slice(5).trim()); - } - } - - const data = dataLines.join('\n'); - - if (eventName === 'stdout') { - handlers?.onStdout?.(data.replace(/\\n/g, '\n')); - return; - } - - if (eventName === 'stderr') { - handlers?.onStderr?.(data.replace(/\\n/g, '\n')); - return; - } - - if (eventName === 'error' || eventName === 'cancelled') { - handlers?.onError?.(data.replace(/\\n/g, '\n')); - return; - } - - if (eventName === 'complete') { - completedResponse = JSON.parse(data) as CommandExecutionResponse; - } - }; - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - const events = buffer.split('\n\n'); - buffer = events.pop() ?? ''; - - for (const rawEvent of events) { - if (rawEvent.trim()) { - parseEvent(rawEvent); - } - } - } - - if (!completedResponse) { - throw new Error('Streaming execution completed without a final response payload'); - } - - return completedResponse; - } - - async executeIterativeCommandWithStreaming( - request: CommandExecutionRequest, - handlers?: IterativeStreamingHandlers, - signal?: AbortSignal - ): Promise { - const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5081'; - const response = await fetch(`${apiBaseUrl}/api/commands/execute-iterative-stream`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'text/event-stream' - }, - body: JSON.stringify(request), - signal - }); - - if (response.status === 404) { - handlers?.onError?.('Iterative streaming endpoint not found. Falling back to non-streaming iterative execution.'); - return this.executeIterativeCommand(request); - } - - if (!response.ok || !response.body) { - throw new Error(`Failed to start iterative streaming execution (${response.status})`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let completedResponse: IterationExecutionResponse | null = null; - - const parseEvent = (rawEvent: string) => { - const lines = rawEvent.split('\n'); - let eventName = 'message'; - const dataLines: string[] = []; - - for (const line of lines) { - if (line.startsWith('event:')) { - eventName = line.slice(6).trim(); - } - - if (line.startsWith('data:')) { - dataLines.push(line.slice(5).trim()); - } - } - - const data = dataLines.join('\n'); - - if (eventName === 'item-start') { - const parsed = JSON.parse(data) as { itemPath: string; itemName: string }; - handlers?.onItemStart?.(parsed.itemPath, parsed.itemName); - return; - } - - if (eventName === 'stdout') { - const parsed = JSON.parse(data) as { itemPath: string; line: string }; - handlers?.onStdout?.(parsed.itemPath, parsed.line); - return; - } - - if (eventName === 'stderr') { - const parsed = JSON.parse(data) as { itemPath: string; line: string }; - handlers?.onStderr?.(parsed.itemPath, parsed.line); - return; - } - - if (eventName === 'progress') { - const parsed = JSON.parse(data) as Record; - handlers?.onProgress?.({ - totalItems: Number(parsed.totalItems ?? parsed.TotalItems ?? 0), - processedItems: Number(parsed.processedItems ?? parsed.ProcessedItems ?? 0), - successfulItems: Number(parsed.successfulItems ?? parsed.SuccessfulItems ?? 0), - failedItems: Number(parsed.failedItems ?? parsed.FailedItems ?? 0), - skippedItems: Number(parsed.skippedItems ?? parsed.SkippedItems ?? 0), - currentItem: String(parsed.currentItem ?? parsed.CurrentItem ?? ''), - currentDirectory: String(parsed.currentDirectory ?? parsed.CurrentDirectory ?? ''), - isCompleted: Boolean(parsed.isCompleted ?? parsed.IsCompleted ?? false), - wasCancelled: Boolean(parsed.wasCancelled ?? parsed.WasCancelled ?? false), - startedAt: String(parsed.startedAt ?? parsed.StartedAt ?? ''), - completedAt: parsed.completedAt ?? parsed.CompletedAt - ? String(parsed.completedAt ?? parsed.CompletedAt) - : undefined, - }); - return; - } - - if (eventName === 'item-complete') { - handlers?.onItemComplete?.(JSON.parse(data)); - return; - } - - if (eventName === 'error' || eventName === 'cancelled') { - handlers?.onError?.(data.replace(/\\n/g, '\n')); - return; - } - - if (eventName === 'complete') { - completedResponse = JSON.parse(data) as IterationExecutionResponse; - } - }; - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - const events = buffer.split('\n\n'); - buffer = events.pop() ?? ''; - - for (const rawEvent of events) { - if (rawEvent.trim()) { - parseEvent(rawEvent); - } - } - } - - if (!completedResponse) { - throw new Error('Iterative streaming execution completed without a final response payload'); - } - - return completedResponse; - } -} - -export const commandsApi = new CommandsApiService(); diff --git a/src/CommandRunner.ReactWebsite/src/services/api/directories.ts b/src/CommandRunner.ReactWebsite/src/services/api/directories.ts deleted file mode 100644 index d718332..0000000 --- a/src/CommandRunner.ReactWebsite/src/services/api/directories.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { apiClient } from './client'; -import { FavoriteDirectoryDto } from './types'; - -export class DirectoriesApiService { - // Get all favorite directories - async getAllDirectories(): Promise { - return apiClient.get('/api/directories'); - } - - // Get directory by ID - async getDirectory(id: string): Promise { - return apiClient.get(`/api/directories/${id}`); - } - - // Get directory by path - async getDirectoryByPath(path: string): Promise { - return apiClient.get(`/api/directories/by-path`, { path }); - } - - // Get most used directories - async getMostUsedDirectories(count: number = 10): Promise { - return apiClient.get(`/api/directories/most-used`, { count }); - } - - // Create new favorite directory - async createDirectory(directory: Omit): Promise { - return apiClient.post('/api/directories', directory); - } - - // Update directory - async updateDirectory(id: string, directory: Partial): Promise { - return apiClient.put(`/api/directories/${id}`, directory); - } - - // Delete directory - async deleteDirectory(id: string): Promise { - return apiClient.delete(`/api/directories/${id}`); - } - - // Increment usage count - async incrementUsage(id: string): Promise { - return apiClient.post(`/api/directories/${id}/increment-usage`); - } -} - -export const directoriesApi = new DirectoriesApiService(); \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/src/services/api/profiles.ts b/src/CommandRunner.ReactWebsite/src/services/api/profiles.ts deleted file mode 100644 index 0e392fc..0000000 --- a/src/CommandRunner.ReactWebsite/src/services/api/profiles.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { apiClient } from './client'; -import { ProfileDto, CommandDto } from './types'; - -export class ProfilesApiService { - // Get all profiles - async getAllProfiles(): Promise { - return apiClient.get('/api/profiles'); - } - - // Get profile by ID - async getProfile(id: string): Promise { - return apiClient.get(`/api/profiles/${id}`); - } - - // Get profile by name - async getProfileByName(name: string): Promise { - return apiClient.get(`/api/profiles/by-name/${encodeURIComponent(name)}`); - } - - // Get favorite profiles - async getFavoriteProfiles(): Promise { - return apiClient.get('/api/profiles/favorites'); - } - - // Create new profile - async createProfile(profile: ProfileDto): Promise { - return apiClient.post('/api/profiles', profile); - } - - // Import profile from JSON file - async importProfile(file: File): Promise { - const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5081'; - const formData = new FormData(); - formData.append('file', file); - - const response = await fetch(`${apiBaseUrl}/api/profiles/import`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || `Failed to import profile (${response.status})`); - } - - return response.json() as Promise; - } - - // Update profile - async updateProfile(id: string, profile: Partial): Promise { - return apiClient.put(`/api/profiles/${id}`, profile); - } - - // Delete profile - async deleteProfile(id: string): Promise { - return apiClient.delete(`/api/profiles/${id}`); - } - - // Add command to profile - async addCommandToProfile(profileId: string, command: Omit): Promise { - const profile = await this.getProfile(profileId); - const newCommand = { - ...command, - id: crypto.randomUUID(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - profile.commands.push(newCommand); - await this.updateProfile(profileId, profile); - return this.getProfile(profileId); - } - - // Update command in profile - async updateCommandInProfile(profileId: string, commandId: string, command: Partial): Promise { - const profile = await this.getProfile(profileId); - const commandIndex = profile.commands.findIndex(c => c.id === commandId); - if (commandIndex === -1) { - throw new Error('Command not found in profile'); - } - profile.commands[commandIndex] = { - ...profile.commands[commandIndex], - ...command, - updatedAt: new Date().toISOString(), - }; - await this.updateProfile(profileId, profile); - return this.getProfile(profileId); - } - - // Remove command from profile - async removeCommandFromProfile(profileId: string, commandId: string): Promise { - const profile = await this.getProfile(profileId); - profile.commands = profile.commands.filter(c => c.id !== commandId); - await this.updateProfile(profileId, profile); - return this.getProfile(profileId); - } -} - -export const profilesApi = new ProfilesApiService(); diff --git a/src/CommandRunner.ReactWebsite/src/themes/index.ts b/src/CommandRunner.ReactWebsite/src/themes/index.ts deleted file mode 100644 index dac650d..0000000 --- a/src/CommandRunner.ReactWebsite/src/themes/index.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { createTheme } from '@mui/material/styles'; - -// Base theme configuration -const baseTheme = { - typography: { - fontFamily: '"Inter", "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif', - h6: { - fontWeight: 600, - fontSize: '1.125rem', - }, - body2: { - fontSize: '0.875rem', - }, - }, - shape: { - borderRadius: 8, - }, - components: { - MuiPaper: { - styleOverrides: { - root: { - backgroundImage: 'none', - }, - }, - }, - MuiButton: { - styleOverrides: { - root: { - textTransform: 'none', - fontWeight: 500, - borderRadius: 6, - boxShadow: 'none', - transition: 'all 0.2s ease', - }, - }, - }, - MuiTextField: { - styleOverrides: { - root: { - '& .MuiFilledInput-root': { - borderRadius: 6, - }, - }, - }, - }, - MuiSelect: { - styleOverrides: { - root: { - '& .MuiFilledInput-root': { - borderRadius: 6, - }, - }, - }, - }, - }, -}; - -// Light theme -export const lightTheme = createTheme({ - ...baseTheme, - palette: { - mode: 'light', - primary: { - main: '#2563eb', - }, - secondary: { - main: '#64748b', - }, - background: { - default: '#f8fafc', - paper: '#ffffff', - }, - text: { - primary: '#1e293b', - secondary: '#64748b', - }, - }, - components: { - ...baseTheme.components, - MuiPaper: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiPaper?.styleOverrides?.root, - border: '1px solid #e2e8f0', - boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)', - }, - }, - }, - MuiButton: { - styleOverrides: { - ...baseTheme.components?.MuiButton?.styleOverrides?.root, - contained: { - backgroundColor: '#2563eb', - color: '#ffffff', - '&:hover': { - backgroundColor: '#1d4ed8', - }, - }, - outlined: { - borderColor: '#cbd5e1', - color: '#475569', - '&:hover': { - borderColor: '#2563eb', - backgroundColor: '#f1f5f9', - }, - }, - }, - }, - MuiTextField: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiTextField?.styleOverrides?.root, - '& .MuiFilledInput-root': { - ...baseTheme.components?.MuiTextField?.styleOverrides?.root?.['& .MuiFilledInput-root'], - backgroundColor: '#f8fafc', - border: '1px solid #cbd5e1', - '&:hover': { - backgroundColor: '#f1f5f9', - borderColor: '#2563eb', - }, - '&.Mui-focused': { - backgroundColor: '#f1f5f9', - borderColor: '#2563eb', - boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.1)', - }, - '&::before': { - borderBottom: 'none', - }, - '&::after': { - borderBottom: 'none', - }, - }, - '& .MuiInputLabel-root': { - color: '#64748b', - '&.Mui-focused': { - color: '#2563eb', - }, - }, - }, - }, - }, - MuiSelect: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiSelect?.styleOverrides?.root, - '& .MuiFilledInput-root': { - ...baseTheme.components?.MuiSelect?.styleOverrides?.root?.['& .MuiFilledInput-root'], - backgroundColor: '#f8fafc', - border: '1px solid #cbd5e1', - '&:hover': { - backgroundColor: '#f1f5f9', - borderColor: '#2563eb', - }, - '&.Mui-focused': { - backgroundColor: '#f1f5f9', - borderColor: '#2563eb', - boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.1)', - }, - '&::before': { - borderBottom: 'none', - }, - '&::after': { - borderBottom: 'none', - }, - }, - '& .MuiInputLabel-root': { - color: '#64748b', - '&.Mui-focused': { - color: '#2563eb', - }, - }, - }, - }, - }, - MuiAppBar: { - styleOverrides: { - root: { - backgroundColor: '#ffffff', - color: '#1e293b', - borderBottom: '1px solid #e2e8f0', - boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', - }, - }, - }, - }, -}); - -// Dark theme -export const darkTheme = createTheme({ - ...baseTheme, - palette: { - mode: 'dark', - primary: { - main: '#2563eb', - }, - secondary: { - main: '#64748b', - }, - background: { - default: '#0f172a', - paper: '#1e293b', - }, - text: { - primary: '#f1f5f9', - secondary: '#94a3b8', - }, - }, - components: { - ...baseTheme.components, - MuiPaper: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiPaper?.styleOverrides?.root, - border: '1px solid #334155', - boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2)', - }, - }, - }, - MuiButton: { - styleOverrides: { - ...baseTheme.components?.MuiButton?.styleOverrides?.root, - contained: { - backgroundColor: '#2563eb', - color: '#ffffff', - '&:hover': { - backgroundColor: '#1d4ed8', - }, - }, - outlined: { - borderColor: '#475569', - color: '#f1f5f9', - '&:hover': { - borderColor: '#2563eb', - backgroundColor: '#1e293b', - }, - }, - }, - }, - MuiTextField: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiTextField?.styleOverrides?.root, - '& .MuiFilledInput-root': { - ...baseTheme.components?.MuiTextField?.styleOverrides?.root?.['& .MuiFilledInput-root'], - backgroundColor: '#1e293b', - border: '1px solid #475569', - '&:hover': { - backgroundColor: '#334155', - borderColor: '#2563eb', - }, - '&.Mui-focused': { - backgroundColor: '#334155', - borderColor: '#2563eb', - boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.1)', - }, - '&::before': { - borderBottom: 'none', - }, - '&::after': { - borderBottom: 'none', - }, - }, - '& .MuiInputLabel-root': { - color: '#94a3b8', - '&.Mui-focused': { - color: '#2563eb', - }, - }, - }, - }, - }, - MuiSelect: { - styleOverrides: { - root: { - ...baseTheme.components?.MuiSelect?.styleOverrides?.root, - '& .MuiFilledInput-root': { - ...baseTheme.components?.MuiSelect?.styleOverrides?.root?.['& .MuiFilledInput-root'], - backgroundColor: '#1e293b', - border: '1px solid #475569', - '&:hover': { - backgroundColor: '#334155', - borderColor: '#2563eb', - }, - '&.Mui-focused': { - backgroundColor: '#334155', - borderColor: '#2563eb', - boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.1)', - }, - '&::before': { - borderBottom: 'none', - }, - '&::after': { - borderBottom: 'none', - }, - }, - '& .MuiInputLabel-root': { - color: '#94a3b8', - '&.Mui-focused': { - color: '#2563eb', - }, - }, - }, - }, - }, - MuiAppBar: { - styleOverrides: { - root: { - backgroundColor: '#1e293b', - color: '#f1f5f9', - borderBottom: '1px solid #334155', - boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)', - }, - }, - }, - }, -}); - -// Export theme getter function -export const getTheme = (mode: 'light' | 'dark') => mode === 'light' ? lightTheme : darkTheme; \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/tsconfig.electron.json b/src/CommandRunner.ReactWebsite/tsconfig.electron.json deleted file mode 100644 index 943bf5f..0000000 --- a/src/CommandRunner.ReactWebsite/tsconfig.electron.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "CommonJS", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "electron", - "rootDir": ".", - "types": ["node"] - }, - "include": [ - "electron/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/tsconfig.json b/src/CommandRunner.ReactWebsite/tsconfig.json deleted file mode 100644 index fd1e5da..0000000 --- a/src/CommandRunner.ReactWebsite/tsconfig.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - /* Path mapping */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - }, - - /* Type definitions */ - "types": ["vite/client"] - }, - "include": [ - "src", - "src/**/*.ts", - "src/**/*.tsx", - "electron", - "electron/**/*.ts" - ], - "exclude": [ - "node_modules", - "dist" - ], - "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/tsconfig.node.json b/src/CommandRunner.ReactWebsite/tsconfig.node.json deleted file mode 100644 index 4eb43d0..0000000 --- a/src/CommandRunner.ReactWebsite/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -} \ No newline at end of file diff --git a/src/CommandRunner.ReactWebsite/vite.config.ts b/src/CommandRunner.ReactWebsite/vite.config.ts deleted file mode 100644 index e267766..0000000 --- a/src/CommandRunner.ReactWebsite/vite.config.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { VitePWA } from 'vite-plugin-pwa' -import * as path from 'path' - -// https://vitejs.dev/config/ -export default defineConfig(({ mode }) => { - const isElectron = mode === 'electron'; - - return { - plugins: [ - react(), - // Only include PWA plugin for web builds, not Electron - ...(isElectron ? [] : [VitePWA({ - registerType: 'autoUpdate', - includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'], - manifest: { - name: 'Command Runner', - short_name: 'CmdRunner', - description: 'Cross-platform command execution tool', - theme_color: '#1976d2', - background_color: '#ffffff', - display: 'standalone', - icons: [ - { - src: 'pwa-192x192.png', - sizes: '192x192', - type: 'image/png' - }, - { - src: 'pwa-512x512.png', - sizes: '512x512', - type: 'image/png' - } - ] - }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'] - } - })]) - ], - base: isElectron ? './' : '/', - build: { - outDir: isElectron ? 'dist' : 'dist', - assetsDir: 'assets', - rollupOptions: { - input: { - main: path.resolve(__dirname, 'index.html') - } - } - }, - define: { - // Define environment variables for Electron - 'process.env.IS_ELECTRON': JSON.stringify(isElectron) - } - }; -}) \ No newline at end of file diff --git a/src/CommandRunner.Website.VueJs/eslint.config.js b/src/CommandRunner.Website.VueJs/eslint.config.js new file mode 100644 index 0000000..d9e3e9a --- /dev/null +++ b/src/CommandRunner.Website.VueJs/eslint.config.js @@ -0,0 +1,17 @@ +import pluginVue from 'eslint-plugin-vue'; +import vueTsEslintConfig from '@vue/eslint-config-typescript'; + +export default [ + { ignores: ['dist/**'] }, + ...pluginVue.configs['flat/recommended'], + ...vueTsEslintConfig(), + { + rules: { + 'vue/multi-word-component-names': 'off', + // Formatting-only rules; we're not running Prettier over templates. + 'vue/max-attributes-per-line': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/html-self-closing': 'off', + }, + }, +]; diff --git a/src/CommandRunner.Website.VueJs/index.html b/src/CommandRunner.Website.VueJs/index.html new file mode 100644 index 0000000..17f8b89 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/index.html @@ -0,0 +1,12 @@ + + + + + + Command Runner + + +
+ + + diff --git a/src/CommandRunner.Website.VueJs/package-lock.json b/src/CommandRunner.Website.VueJs/package-lock.json new file mode 100644 index 0000000..9a70410 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/package-lock.json @@ -0,0 +1,3539 @@ +{ + "name": "commandrunner-website-vuejs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "commandrunner-website-vuejs", + "version": "0.0.0", + "dependencies": { + "pinia": "^2.2.6", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-typescript": "^14.1.4", + "eslint": "^9.16.0", + "eslint-plugin-vue": "^9.32.0", + "typescript": "~5.6.3", + "vite": "^6.0.3", + "vue-tsc": "^2.1.10" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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==", + "license": "MIT" + }, + "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/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz", + "integrity": "sha512-E3j9hDlfVf10F30MRcLTPY2IIhWIx1nsvkVukk14kTcuA+oBVot9zsP1hzsO+PAMDxV3Fd9FimBJtUBNBL5KFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.60.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.60.0", + "vue-eslint-parser": "^10.4.0" + }, + "bin": { + "vue-eslint-config-typescript": "dist/bin.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/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/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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-glob/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/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/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-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-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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "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/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyglobby/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", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_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/vite/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", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/CommandRunner.Website.VueJs/package.json b/src/CommandRunner.Website.VueJs/package.json new file mode 100644 index 0000000..8d64b5b --- /dev/null +++ b/src/CommandRunner.Website.VueJs/package.json @@ -0,0 +1,27 @@ +{ + "name": "commandrunner-website-vuejs", + "private": true, + "version": "0.0.0", + "description": "Cross-platform command execution tool (Vue frontend)", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --max-warnings 0", + "type-check": "vue-tsc -b --noEmit" + }, + "dependencies": { + "pinia": "^2.2.6", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-typescript": "^14.1.4", + "eslint": "^9.16.0", + "eslint-plugin-vue": "^9.32.0", + "typescript": "~5.6.3", + "vite": "^6.0.3", + "vue-tsc": "^2.1.10" + } +} diff --git a/src/CommandRunner.Website.VueJs/src/App.vue b/src/CommandRunner.Website.VueJs/src/App.vue new file mode 100644 index 0000000..b52ed59 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/App.vue @@ -0,0 +1,461 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/AlertBanner.vue b/src/CommandRunner.Website.VueJs/src/components/AlertBanner.vue new file mode 100644 index 0000000..22c6e3a --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/AlertBanner.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/AppIcon.vue b/src/CommandRunner.Website.VueJs/src/components/AppIcon.vue new file mode 100644 index 0000000..543544c --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/AppIcon.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandCard.vue b/src/CommandRunner.Website.VueJs/src/components/CommandCard.vue new file mode 100644 index 0000000..a1fb741 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandCard.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandControls.vue b/src/CommandRunner.Website.VueJs/src/components/CommandControls.vue new file mode 100644 index 0000000..3af6a11 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandControls.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandDetails.vue b/src/CommandRunner.Website.VueJs/src/components/CommandDetails.vue new file mode 100644 index 0000000..5951f4e --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandDetails.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandForm.vue b/src/CommandRunner.Website.VueJs/src/components/CommandForm.vue new file mode 100644 index 0000000..271e7f0 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandPreview.vue b/src/CommandRunner.Website.VueJs/src/components/CommandPreview.vue new file mode 100644 index 0000000..9051b00 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandPreview.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/CommandSelector.vue b/src/CommandRunner.Website.VueJs/src/components/CommandSelector.vue new file mode 100644 index 0000000..443e854 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/CommandSelector.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/DirectorySelector.vue b/src/CommandRunner.Website.VueJs/src/components/DirectorySelector.vue new file mode 100644 index 0000000..59c4005 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/DirectorySelector.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/FormattedOutput.vue b/src/CommandRunner.Website.VueJs/src/components/FormattedOutput.vue new file mode 100644 index 0000000..bbabdea --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/FormattedOutput.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/HeaderButtons.vue b/src/CommandRunner.Website.VueJs/src/components/HeaderButtons.vue new file mode 100644 index 0000000..dddebb0 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/HeaderButtons.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/OutputPanel.vue b/src/CommandRunner.Website.VueJs/src/components/OutputPanel.vue new file mode 100644 index 0000000..e94c249 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/OutputPanel.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/ProfileCard.vue b/src/CommandRunner.Website.VueJs/src/components/ProfileCard.vue new file mode 100644 index 0000000..44f320e --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/ProfileCard.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/ProfileForm.vue b/src/CommandRunner.Website.VueJs/src/components/ProfileForm.vue new file mode 100644 index 0000000..656b1c9 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/ProfileForm.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/ProfileSelector.vue b/src/CommandRunner.Website.VueJs/src/components/ProfileSelector.vue new file mode 100644 index 0000000..941cff2 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/ProfileSelector.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/SettingsDialog.vue b/src/CommandRunner.Website.VueJs/src/components/SettingsDialog.vue new file mode 100644 index 0000000..45ba8d1 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/SettingsDialog.vue @@ -0,0 +1,381 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/ToastNotification.vue b/src/CommandRunner.Website.VueJs/src/components/ToastNotification.vue new file mode 100644 index 0000000..8074098 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/ToastNotification.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/components/ToggleSwitch.vue b/src/CommandRunner.Website.VueJs/src/components/ToggleSwitch.vue new file mode 100644 index 0000000..01961e2 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/components/ToggleSwitch.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/CommandRunner.Website.VueJs/src/composables/useKeyboardShortcuts.ts b/src/CommandRunner.Website.VueJs/src/composables/useKeyboardShortcuts.ts new file mode 100644 index 0000000..c637ec5 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/composables/useKeyboardShortcuts.ts @@ -0,0 +1,58 @@ +import { onMounted, onUnmounted } from 'vue'; + +export interface KeyboardShortcutHandlers { + onExecute: () => void; + onClearOutput: () => void; + onToggleFocusMode: () => void; + onBrowseDirectory: () => void; + onOpenSettings: () => void; + /** Whether a command can currently be executed (mirrors the Run button's disabled state). */ + canExecute: () => boolean; +} + +/** + * Desktop-style shortcuts: Ctrl/Cmd+Enter run, Ctrl/Cmd+L clear, F11 focus mode, + * Ctrl/Cmd+B browse directory, Ctrl/Cmd+, settings. Ignored while typing in a form field. + */ +export function useKeyboardShortcuts(handlers: KeyboardShortcutHandlers) { + const handleKeyDown = (event: KeyboardEvent) => { + const target = event.target as HTMLElement | null; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) { + return; + } + + const modifier = event.ctrlKey || event.metaKey; + + if (modifier && event.key === 'Enter') { + event.preventDefault(); + if (handlers.canExecute()) handlers.onExecute(); + return; + } + + if (modifier && event.key.toLowerCase() === 'l') { + event.preventDefault(); + handlers.onClearOutput(); + return; + } + + if (event.key === 'F11') { + event.preventDefault(); + handlers.onToggleFocusMode(); + return; + } + + if (modifier && event.key.toLowerCase() === 'b') { + event.preventDefault(); + handlers.onBrowseDirectory(); + return; + } + + if (modifier && event.key === ',') { + event.preventDefault(); + handlers.onOpenSettings(); + } + }; + + onMounted(() => document.addEventListener('keydown', handleKeyDown)); + onUnmounted(() => document.removeEventListener('keydown', handleKeyDown)); +} diff --git a/src/CommandRunner.Website.VueJs/src/composables/useOutputFormatter.ts b/src/CommandRunner.Website.VueJs/src/composables/useOutputFormatter.ts new file mode 100644 index 0000000..62fca21 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/composables/useOutputFormatter.ts @@ -0,0 +1,120 @@ +export interface PlainOutput { + type: 'plain'; + content: string; +} + +export interface ErrorOutput { + type: 'error'; + title: string; + message: string; + suggestions: string[]; + fullDetails: string; +} + +export type FormattedOutput = PlainOutput | ErrorOutput; + +/** + * Recognizes a handful of common failure shapes (.NET exceptions, command-not-found, + * permission, connection, filesystem errors) and turns them into a friendlier summary + * with actionable suggestions, falling back to plain text otherwise. + */ +export function formatOutput(text: string): FormattedOutput { + if (!text) return { type: 'plain', content: text }; + + const lines = text.split('\n'); + + const isDotNetException = lines.some((line) => line.includes('System.') && line.includes('Exception')); + if (isDotNetException) { + const mainException = lines.find( + (line) => line.trim().startsWith('System.') && line.includes('Exception:') && !line.includes('--->'), + ); + const unhandledException = lines.find( + (line) => line.includes('Unhandled exception') || line.includes('An unhandled exception'), + ); + + let message = 'An error occurred'; + if (mainException) { + const colonIndex = mainException.indexOf('Exception:'); + if (colonIndex !== -1) { + message = mainException.substring(colonIndex + 11).trim(); + } + } else if (unhandledException) { + message = unhandledException.trim(); + } + + return { + type: 'error', + title: 'Application Error', + message, + suggestions: [ + 'Check if required services are running', + 'Verify configuration settings', + 'Ensure all dependencies are installed', + 'Check file and network permissions', + ], + fullDetails: text, + }; + } + + if (lines.some((line) => line.includes('command not found') || line.includes('is not recognized') || line.includes('No such file or directory'))) { + return { + type: 'error', + title: 'Command Not Found', + message: 'The specified command could not be located', + suggestions: [ + 'Install the required software', + 'Use absolute paths to executables', + 'Check if the command is in your PATH', + 'Verify the executable name is correct', + ], + fullDetails: text, + }; + } + + if (lines.some((line) => line.includes('Permission denied') || line.includes('Access is denied') || line.includes('Operation not permitted'))) { + return { + type: 'error', + title: 'Permission Denied', + message: 'Access to the requested resource was denied', + suggestions: [ + 'Run with elevated privileges (sudo/admin)', + 'Check file ownership and permissions', + 'Verify access to the target directory', + 'Ensure the user has necessary rights', + ], + fullDetails: text, + }; + } + + if (lines.some((line) => line.includes('Connection refused') || line.includes('Network is unreachable') || line.includes('Address already in use'))) { + return { + type: 'error', + title: 'Connection Error', + message: 'Network or connection-related error occurred', + suggestions: [ + 'Check if the target service is running', + 'Verify network connectivity', + 'Ensure the correct port/address is used', + 'Check firewall settings', + ], + fullDetails: text, + }; + } + + if (lines.some((line) => line.includes('No such file') || line.includes('Directory not found') || line.includes('File exists'))) { + return { + type: 'error', + title: 'File System Error', + message: 'File or directory operation failed', + suggestions: [ + 'Verify the file/directory path exists', + 'Check file permissions', + 'Use absolute paths instead of relative paths', + 'Ensure the working directory is correct', + ], + fullDetails: text, + }; + } + + return { type: 'plain', content: text }; +} diff --git a/src/CommandRunner.Website.VueJs/src/composables/useTheme.ts b/src/CommandRunner.Website.VueJs/src/composables/useTheme.ts new file mode 100644 index 0000000..d4b931d --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/composables/useTheme.ts @@ -0,0 +1,26 @@ +import { watch } from 'vue'; +import { useAppStore, type ThemeMode } from '../stores/app'; + +const STORAGE_KEY = 'theme'; + +/** Wires the store's theme to and localStorage; call once from App.vue. */ +export function useTheme() { + const store = useAppStore(); + + const saved = localStorage.getItem(STORAGE_KEY); + store.setTheme(saved === 'light' || saved === 'dark' ? saved : 'dark'); + + const applyTheme = (mode: ThemeMode) => { + document.documentElement.dataset.theme = mode; + localStorage.setItem(STORAGE_KEY, mode); + }; + + applyTheme(store.theme); + watch(() => store.theme, applyTheme); + + const toggleTheme = () => { + store.setTheme(store.theme === 'light' ? 'dark' : 'light'); + }; + + return { toggleTheme }; +} diff --git a/src/CommandRunner.Website.VueJs/src/main.ts b/src/CommandRunner.Website.VueJs/src/main.ts new file mode 100644 index 0000000..eaee3e2 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import './styles/base.css'; + +createApp(App).use(createPinia()).mount('#app'); diff --git a/src/CommandRunner.Website.VueJs/src/services/api/client.ts b/src/CommandRunner.Website.VueJs/src/services/api/client.ts new file mode 100644 index 0000000..74ad3c5 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/services/api/client.ts @@ -0,0 +1,109 @@ +// Thin fetch wrapper: JSON in/out plus consistent, status-code-aware error messages. + +// No env override => same origin the page was served from. This is what makes the Photino +// desktop host work without any configuration: it serves the Vue build and the API from the +// same dynamically-assigned Kestrel port, so relative/same-origin requests always find it +// regardless of which port got picked. Only the standalone web-dev workflow (Vite dev server +// on one port, API on another) needs VITE_API_BASE_URL set explicitly. +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || window.location.origin; + +export class ApiError extends Error { + constructor( + message: string, + public readonly status?: number, + ) { + super(message); + this.name = 'ApiError'; + } +} + +function buildUrl(path: string, params?: Record): string { + const url = new URL(path, API_BASE_URL); + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + } + } + return url.toString(); +} + +async function extractErrorMessage(response: Response): Promise { + let message = 'API Error'; + + try { + const contentType = response.headers.get('content-type') ?? ''; + if (contentType.includes('application/json')) { + const data = await response.json(); + message = data?.message ?? data?.error ?? data?.title ?? message; + } else { + const text = await response.text(); + if (text) message = text; + } + } catch { + // Body wasn't readable/parseable — fall back to the generic message below. + } + + switch (response.status) { + case 400: + return `Bad Request: ${message}`; + case 401: + return `Unauthorized: ${message}`; + case 403: + return `Forbidden: ${message}`; + case 404: + return `Not Found: ${message}`; + case 409: + return `Conflict: ${message}`; + default: + return response.status >= 500 ? `Server Error: ${message}` : message; + } +} + +async function request(path: string, init?: RequestInit, params?: Record): Promise { + let response: Response; + try { + response = await fetch(buildUrl(path, params), { + ...init, + headers: { + 'Content-Type': 'application/json', + ...init?.headers, + }, + }); + } catch { + throw new ApiError('Network error - please check your connection'); + } + + if (!response.ok) { + throw new ApiError(await extractErrorMessage(response), response.status); + } + + if (response.status === 204) { + return undefined as T; + } + + const contentType = response.headers.get('content-type') ?? ''; + if (!contentType.includes('application/json')) { + return undefined as T; + } + + return (await response.json()) as T; +} + +export const apiClient = { + get(path: string, params?: Record): Promise { + return request(path, { method: 'GET' }, params); + }, + post(path: string, data?: unknown): Promise { + return request(path, { method: 'POST', body: data !== undefined ? JSON.stringify(data) : undefined }); + }, + put(path: string, data?: unknown): Promise { + return request(path, { method: 'PUT', body: data !== undefined ? JSON.stringify(data) : undefined }); + }, + delete(path: string): Promise { + return request(path, { method: 'DELETE' }); + }, +}; + +export { API_BASE_URL }; diff --git a/src/CommandRunner.Website.VueJs/src/services/api/commands.ts b/src/CommandRunner.Website.VueJs/src/services/api/commands.ts new file mode 100644 index 0000000..69ce961 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/services/api/commands.ts @@ -0,0 +1,227 @@ +import { apiClient, API_BASE_URL } from './client'; +import type { + CommandExecutionRequest, + CommandExecutionResponse, + IterationExecutionResponse, + ValidationResult, +} from './types'; + +export interface StreamingHandlers { + onStdout?: (line: string) => void; + onStderr?: (line: string) => void; + onError?: (message: string) => void; +} + +export interface IterationProgressEvent { + totalItems: number; + processedItems: number; + successfulItems: number; + failedItems: number; + skippedItems: number; + currentItem: string; + currentDirectory: string; + isCompleted: boolean; + wasCancelled: boolean; + startedAt: string; + completedAt?: string; +} + +export interface IterationItemCompleteEvent { + itemPath: string; + wasSuccessful: boolean; + errorMessage?: string; + output: string; + errorOutput: string; + executionTime?: string; + executedAt: string; +} + +export interface IterativeStreamingHandlers { + onItemStart?: (itemPath: string, itemName: string) => void; + onStdout?: (itemPath: string, line: string) => void; + onStderr?: (itemPath: string, line: string) => void; + onProgress?: (progress: IterationProgressEvent) => void; + onItemComplete?: (item: IterationItemCompleteEvent) => void; + onError?: (message: string) => void; +} + +/** Splits a raw SSE frame ("event: x\ndata: y\n\n") into its event name and data payload. */ +function parseSseFrame(rawEvent: string): { eventName: string; data: string } { + const lines = rawEvent.split('\n'); + let eventName = 'message'; + const dataLines: string[] = []; + + for (const line of lines) { + if (line.startsWith('event:')) { + eventName = line.slice(6).trim(); + } + if (line.startsWith('data:')) { + dataLines.push(line.slice(5).trim()); + } + } + + return { eventName, data: dataLines.join('\n') }; +} + +async function* readSseFrames(response: Response): AsyncGenerator<{ eventName: string; data: string }> { + if (!response.body) { + throw new Error('Response has no body to stream'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const frames = buffer.split('\n\n'); + buffer = frames.pop() ?? ''; + + for (const rawFrame of frames) { + if (rawFrame.trim()) { + yield parseSseFrame(rawFrame); + } + } + } +} + +function parseProgressPayload(data: string): IterationProgressEvent { + const parsed = JSON.parse(data) as Record; + return { + totalItems: Number(parsed.totalItems ?? parsed.TotalItems ?? 0), + processedItems: Number(parsed.processedItems ?? parsed.ProcessedItems ?? 0), + successfulItems: Number(parsed.successfulItems ?? parsed.SuccessfulItems ?? 0), + failedItems: Number(parsed.failedItems ?? parsed.FailedItems ?? 0), + skippedItems: Number(parsed.skippedItems ?? parsed.SkippedItems ?? 0), + currentItem: String(parsed.currentItem ?? parsed.CurrentItem ?? ''), + currentDirectory: String(parsed.currentDirectory ?? parsed.CurrentDirectory ?? ''), + isCompleted: Boolean(parsed.isCompleted ?? parsed.IsCompleted ?? false), + wasCancelled: Boolean(parsed.wasCancelled ?? parsed.WasCancelled ?? false), + startedAt: String(parsed.startedAt ?? parsed.StartedAt ?? ''), + completedAt: + parsed.completedAt ?? parsed.CompletedAt ? String(parsed.completedAt ?? parsed.CompletedAt) : undefined, + }; +} + +export const commandsApi = { + executeCommand(request: CommandExecutionRequest): Promise { + return apiClient.post('/api/commands/execute', request); + }, + + executeIterativeCommand(request: CommandExecutionRequest): Promise { + return apiClient.post('/api/commands/execute-iterative', request); + }, + + validateCommand(profileId: string, commandId: string): Promise { + return apiClient.post(`/api/commands/validate/${profileId}/${commandId}`); + }, + + async executeCommandWithStreaming( + request: CommandExecutionRequest, + handlers?: StreamingHandlers, + signal?: AbortSignal, + ): Promise { + const response = await fetch(`${API_BASE_URL}/api/commands/execute-stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, + body: JSON.stringify(request), + signal, + }); + + if (!response.ok || !response.body) { + throw new Error(`Failed to start streaming command execution (${response.status})`); + } + + let completedResponse: CommandExecutionResponse | null = null; + + for await (const { eventName, data } of readSseFrames(response)) { + switch (eventName) { + case 'stdout': + handlers?.onStdout?.(data.replace(/\\n/g, '\n')); + break; + case 'stderr': + handlers?.onStderr?.(data.replace(/\\n/g, '\n')); + break; + case 'error': + case 'cancelled': + handlers?.onError?.(data.replace(/\\n/g, '\n')); + break; + case 'complete': + completedResponse = JSON.parse(data) as CommandExecutionResponse; + break; + } + } + + if (!completedResponse) { + throw new Error('Streaming execution completed without a final response payload'); + } + + return completedResponse; + }, + + async executeIterativeCommandWithStreaming( + request: CommandExecutionRequest, + handlers?: IterativeStreamingHandlers, + signal?: AbortSignal, + ): Promise { + const response = await fetch(`${API_BASE_URL}/api/commands/execute-iterative-stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, + body: JSON.stringify(request), + signal, + }); + + if (response.status === 404) { + handlers?.onError?.('Iterative streaming endpoint not found. Falling back to non-streaming iterative execution.'); + return commandsApi.executeIterativeCommand(request); + } + + if (!response.ok || !response.body) { + throw new Error(`Failed to start iterative streaming execution (${response.status})`); + } + + let completedResponse: IterationExecutionResponse | null = null; + + for await (const { eventName, data } of readSseFrames(response)) { + switch (eventName) { + case 'item-start': { + const parsed = JSON.parse(data) as { itemPath: string; itemName: string }; + handlers?.onItemStart?.(parsed.itemPath, parsed.itemName); + break; + } + case 'stdout': { + const parsed = JSON.parse(data) as { itemPath: string; line: string }; + handlers?.onStdout?.(parsed.itemPath, parsed.line); + break; + } + case 'stderr': { + const parsed = JSON.parse(data) as { itemPath: string; line: string }; + handlers?.onStderr?.(parsed.itemPath, parsed.line); + break; + } + case 'progress': + handlers?.onProgress?.(parseProgressPayload(data)); + break; + case 'item-complete': + handlers?.onItemComplete?.(JSON.parse(data) as IterationItemCompleteEvent); + break; + case 'error': + case 'cancelled': + handlers?.onError?.(data.replace(/\\n/g, '\n')); + break; + case 'complete': + completedResponse = JSON.parse(data) as IterationExecutionResponse; + break; + } + } + + if (!completedResponse) { + throw new Error('Iterative streaming execution completed without a final response payload'); + } + + return completedResponse; + }, +}; diff --git a/src/CommandRunner.Website.VueJs/src/services/api/directories.ts b/src/CommandRunner.Website.VueJs/src/services/api/directories.ts new file mode 100644 index 0000000..b217c7a --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/services/api/directories.ts @@ -0,0 +1,36 @@ +import { apiClient } from './client'; +import type { FavoriteDirectoryDto } from './types'; + +export const directoriesApi = { + getAllDirectories(): Promise { + return apiClient.get('/api/directories'); + }, + + getDirectory(id: string): Promise { + return apiClient.get(`/api/directories/${id}`); + }, + + getDirectoryByPath(path: string): Promise { + return apiClient.get('/api/directories/by-path', { path }); + }, + + getMostUsedDirectories(count = 10): Promise { + return apiClient.get('/api/directories/most-used', { count }); + }, + + createDirectory(directory: Omit): Promise { + return apiClient.post('/api/directories', directory); + }, + + updateDirectory(id: string, directory: Partial): Promise { + return apiClient.put(`/api/directories/${id}`, directory); + }, + + deleteDirectory(id: string): Promise { + return apiClient.delete(`/api/directories/${id}`); + }, + + incrementUsage(id: string): Promise { + return apiClient.post(`/api/directories/${id}/increment-usage`); + }, +}; diff --git a/src/CommandRunner.ReactWebsite/src/services/api/index.ts b/src/CommandRunner.Website.VueJs/src/services/api/index.ts similarity index 55% rename from src/CommandRunner.ReactWebsite/src/services/api/index.ts rename to src/CommandRunner.Website.VueJs/src/services/api/index.ts index fff7800..1d84bc5 100644 --- a/src/CommandRunner.ReactWebsite/src/services/api/index.ts +++ b/src/CommandRunner.Website.VueJs/src/services/api/index.ts @@ -1,8 +1,5 @@ -// Export all API services -export { apiClient } from './client'; +export { apiClient, ApiError, API_BASE_URL } from './client'; export { profilesApi } from './profiles'; export { commandsApi } from './commands'; export { directoriesApi } from './directories'; - -// Export types -export * from './types'; \ No newline at end of file +export * from './types'; diff --git a/src/CommandRunner.Website.VueJs/src/services/api/profiles.ts b/src/CommandRunner.Website.VueJs/src/services/api/profiles.ts new file mode 100644 index 0000000..f44ffcb --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/services/api/profiles.ts @@ -0,0 +1,45 @@ +import { apiClient, API_BASE_URL } from './client'; +import type { ProfileDto } from './types'; + +export const profilesApi = { + getAllProfiles(): Promise { + return apiClient.get('/api/profiles'); + }, + + getProfile(id: string): Promise { + return apiClient.get(`/api/profiles/${id}`); + }, + + getFavoriteProfiles(): Promise { + return apiClient.get('/api/profiles/favorites'); + }, + + createProfile(profile: ProfileDto): Promise { + return apiClient.post('/api/profiles', profile); + }, + + async importProfile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`${API_BASE_URL}/api/profiles/import`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || `Failed to import profile (${response.status})`); + } + + return response.json() as Promise; + }, + + updateProfile(id: string, profile: Partial): Promise { + return apiClient.put(`/api/profiles/${id}`, profile); + }, + + deleteProfile(id: string): Promise { + return apiClient.delete(`/api/profiles/${id}`); + }, +}; diff --git a/src/CommandRunner.ReactWebsite/src/services/api/types.ts b/src/CommandRunner.Website.VueJs/src/services/api/types.ts similarity index 95% rename from src/CommandRunner.ReactWebsite/src/services/api/types.ts rename to src/CommandRunner.Website.VueJs/src/services/api/types.ts index ff32d20..e908b9e 100644 --- a/src/CommandRunner.ReactWebsite/src/services/api/types.ts +++ b/src/CommandRunner.Website.VueJs/src/services/api/types.ts @@ -1,4 +1,4 @@ -// API Types based on backend DTOs +// API Types based on backend DTOs (CommandRunner.Api/DTOs, CommandRunner.Business/Models) export interface CommandDto { id: string; @@ -32,15 +32,6 @@ export interface FavoriteDirectoryDto { usageCount: number; } -export interface CommandExecutionRequest { - commandId: string; - profileId: string; - workingDirectory?: string; - isIterative?: boolean; - iterationOptions?: IterationOptionsDto; - userConfirmed?: boolean; -} - export interface IterationOptionsDto { skipErrors?: boolean; stopOnFirstFailure?: boolean; @@ -51,6 +42,15 @@ export interface IterationOptionsDto { maxParallelism?: number; } +export interface CommandExecutionRequest { + commandId: string; + profileId: string; + workingDirectory?: string; + isIterative?: boolean; + iterationOptions?: IterationOptionsDto; + userConfirmed?: boolean; +} + export interface CommandExecutionResponse { executionId: string; wasSuccessful: boolean; @@ -63,6 +63,16 @@ export interface CommandExecutionResponse { executionErrors: string[]; } +export interface IterationItemResultDto { + itemPath: string; + wasSuccessful: boolean; + errorMessage?: string; + output: string; + errorOutput: string; + executionTime?: string; + executedAt: string; +} + export interface IterationExecutionResponse { executionId: string; isCompleted: boolean; @@ -77,18 +87,8 @@ export interface IterationExecutionResponse { itemResults: IterationItemResultDto[]; } -export interface IterationItemResultDto { - itemPath: string; - wasSuccessful: boolean; - errorMessage?: string; - output: string; - errorOutput: string; - executionTime?: string; - executedAt: string; -} - export interface ValidationResult { isValid: boolean; errors: string[]; warnings: string[]; -} \ No newline at end of file +} diff --git a/src/CommandRunner.Website.VueJs/src/stores/app.ts b/src/CommandRunner.Website.VueJs/src/stores/app.ts new file mode 100644 index 0000000..614f52e --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/stores/app.ts @@ -0,0 +1,152 @@ +import { defineStore } from 'pinia'; +import { profilesApi, directoriesApi } from '../services/api'; +import type { CommandExecutionResponse, FavoriteDirectoryDto, ProfileDto } from '../services/api'; + +export type ThemeMode = 'light' | 'dark'; + +interface AppState { + profiles: ProfileDto[]; + directories: FavoriteDirectoryDto[]; + selectedProfile: ProfileDto | null; + selectedDirectory: FavoriteDirectoryDto | null; + isLoading: boolean; + creatingProfile: boolean; + error: string | null; + executionHistory: CommandExecutionResponse[]; + theme: ThemeMode; +} + +function describeApiFailure(error: unknown): string { + const err = error as { message?: string; status?: number } | undefined; + + if (err?.status === undefined) { + return 'API server is not running. Please ensure the Command Runner API is started.'; + } + if (err.status >= 500) { + return `API server error (${err.status}): ${err.message ?? 'Unknown server error'}`; + } + return err.message ?? 'Unable to connect to Command Runner API'; +} + +export const useAppStore = defineStore('app', { + state: (): AppState => ({ + profiles: [], + directories: [], + selectedProfile: null, + selectedDirectory: null, + isLoading: false, + creatingProfile: false, + error: null, + executionHistory: [], + theme: 'dark', + }), + + actions: { + async loadInitialData() { + this.isLoading = true; + this.error = null; + + try { + const [profiles, directories] = await Promise.all([ + profilesApi.getAllProfiles(), + directoriesApi.getAllDirectories(), + ]); + this.profiles = profiles; + this.directories = directories; + } catch (error) { + this.profiles = []; + this.directories = []; + this.error = describeApiFailure(error); + } finally { + this.isLoading = false; + } + }, + + setSelectedProfile(profile: ProfileDto | null) { + this.selectedProfile = profile; + }, + + setSelectedDirectory(directory: FavoriteDirectoryDto | null) { + this.selectedDirectory = directory; + }, + + setError(message: string | null) { + this.error = message; + }, + + // Optimistically adds the profile locally, then persists it; rolls back on failure. + async addProfile(profile: ProfileDto) { + if (this.creatingProfile) return; + + this.profiles.push(profile); + this.creatingProfile = true; + + try { + await profilesApi.createProfile(profile); + } catch (error) { + this.profiles = this.profiles.filter((p) => p.id !== profile.id); + this.error = `Failed to save profile: ${describeApiFailure(error)}`; + } finally { + this.creatingProfile = false; + } + }, + + updateProfile(profile: ProfileDto) { + const index = this.profiles.findIndex((p) => p.id === profile.id); + if (index !== -1) { + this.profiles[index] = profile; + } + if (this.selectedProfile?.id === profile.id) { + this.selectedProfile = profile; + } + + profilesApi.updateProfile(profile.id, profile).catch((error) => { + this.error = `Failed to update profile: ${describeApiFailure(error)}`; + }); + }, + + setProfiles(profiles: ProfileDto[]) { + this.profiles = profiles; + }, + + async deleteProfile(id: string) { + const removed = this.profiles.find((p) => p.id === id); + this.profiles = this.profiles.filter((p) => p.id !== id); + if (this.selectedProfile?.id === id) { + this.selectedProfile = null; + } + + try { + await profilesApi.deleteProfile(id); + } catch (error) { + this.error = `Failed to delete profile: ${describeApiFailure(error)}`; + if (removed) { + this.profiles.push(removed); + } + } + }, + + addDirectory(directory: FavoriteDirectoryDto) { + this.directories.push(directory); + }, + + deleteDirectory(id: string) { + this.directories = this.directories.filter((d) => d.id !== id); + if (this.selectedDirectory?.id === id) { + this.selectedDirectory = null; + } + }, + + addExecution(result: CommandExecutionResponse) { + this.executionHistory = [result, ...this.executionHistory].slice(0, 50); + }, + + clearExecutionHistory() { + this.executionHistory = []; + }, + + setTheme(theme: ThemeMode) { + this.theme = theme; + }, + }, +}); diff --git a/src/CommandRunner.Website.VueJs/src/styles/base.css b/src/CommandRunner.Website.VueJs/src/styles/base.css new file mode 100644 index 0000000..fb71744 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/styles/base.css @@ -0,0 +1,232 @@ +@import './tokens.css'; + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; +} + +body { + background-color: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + font-size: 0.875rem; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +#app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: 600; +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + color: inherit; +} + +/* Visible focus ring for keyboard users only; never suppressed. */ +:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ---------------------------------------------------------------------- */ +/* Shared primitives. Components compose these instead of redefining */ +/* buttons/inputs/cards/panels themselves — restyle the whole app here. */ +/* ---------------------------------------------------------------------- */ + +.panel { + background-color: var(--color-bg-elevated); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + padding: var(--space-4); +} + +.panel--compact { + padding: var(--space-2); +} + +.field { + display: flex; + flex-direction: column; + gap: var(--space-1); + min-width: 0; +} + +.field__label { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-secondary); +} + +.field__control { + appearance: none; + width: 100%; + padding: var(--space-2) var(--space-3); + background-color: var(--color-bg-inset); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-sm); + color: var(--color-text); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.field__control:hover:not(:disabled) { + border-color: var(--color-primary); +} + +.field__control:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-soft); +} + +.field__control:disabled { + color: var(--color-text-disabled); + cursor: not-allowed; +} + +.field__control--readonly { + background-color: transparent; + border-style: dashed; + color: var(--color-text-disabled); + cursor: not-allowed; +} + +select.field__control { + background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); + background-position: + calc(100% - 18px) center, + calc(100% - 13px) center; + background-size: + 5px 5px, + 5px 5px; + background-repeat: no-repeat; + padding-right: var(--space-6); + color: var(--color-text-secondary); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-sm); + border: 1px solid transparent; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background-color var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast); + background-color: transparent; + color: var(--color-text); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn--sm { + padding: var(--space-1) var(--space-3); + font-size: 0.8125rem; +} + +.btn--primary { + background-color: var(--color-primary); + color: #ffffff; +} + +.btn--primary:hover:not(:disabled) { + background-color: var(--color-primary-hover); +} + +.btn--outline { + border-color: var(--color-border-strong); +} + +.btn--outline:hover:not(:disabled) { + border-color: var(--color-primary); + background-color: var(--color-bg-hover); +} + +.btn--danger { + color: var(--color-error); +} + +.btn--danger:hover:not(:disabled) { + background-color: rgba(239, 68, 68, 0.12); +} + +.btn--icon { + padding: var(--space-1); + width: 2rem; + height: 2rem; +} + +.chip { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 2px var(--space-2); + border-radius: 999px; + border: 1px solid var(--color-border-strong); + font-size: 0.75rem; + color: var(--color-text-secondary); + white-space: nowrap; +} + +.chip--info { + border-color: var(--color-info); + color: var(--color-info); +} + +.chip--warning { + border-color: var(--color-warning); + color: var(--color-warning); +} + +.chip--success { + border-color: var(--color-success); + color: var(--color-success); +} + +.chip--primary { + border-color: var(--color-primary); + color: var(--color-primary); +} diff --git a/src/CommandRunner.Website.VueJs/src/styles/tokens.css b/src/CommandRunner.Website.VueJs/src/styles/tokens.css new file mode 100644 index 0000000..7b31797 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/src/styles/tokens.css @@ -0,0 +1,70 @@ +/* + * Design tokens. This is the single place to edit to restyle the whole app. + * Every component references these custom properties instead of hard-coded values. + */ + +:root { + color-scheme: dark; + + --color-bg: #0f172a; + --color-bg-elevated: #1e293b; + --color-bg-inset: #0f172a; + --color-bg-hover: #334155; + --color-border: #334155; + --color-border-strong: #475569; + + --color-text: #f1f5f9; + --color-text-secondary: #94a3b8; + --color-text-disabled: #64748b; + + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-primary-soft: rgba(37, 99, 235, 0.16); + + --color-success: #10b981; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-info: #38bdf8; + + --color-focus-ring: #60a5fa; + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.25); + + --radius-sm: 6px; + --radius-md: 8px; + + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + + --font-sans: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + --font-mono: 'SF Mono', Monaco, Inconsolata, 'Fira Code', monospace; + + --transition-fast: 150ms ease; +} + +:root[data-theme='light'] { + color-scheme: light; + + --color-bg: #f8fafc; + --color-bg-elevated: #ffffff; + --color-bg-inset: #f8fafc; + --color-bg-hover: #f1f5f9; + --color-border: #e2e8f0; + --color-border-strong: #cbd5e1; + + --color-text: #1e293b; + --color-text-secondary: #64748b; + --color-text-disabled: #94a3b8; + + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-primary-soft: rgba(37, 99, 235, 0.1); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.05); +} diff --git a/src/CommandRunner.ReactWebsite/src/vite-env.d.ts b/src/CommandRunner.Website.VueJs/src/vite-env.d.ts similarity index 55% rename from src/CommandRunner.ReactWebsite/src/vite-env.d.ts rename to src/CommandRunner.Website.VueJs/src/vite-env.d.ts index d7013dd..d43868c 100644 --- a/src/CommandRunner.ReactWebsite/src/vite-env.d.ts +++ b/src/CommandRunner.Website.VueJs/src/vite-env.d.ts @@ -1,9 +1,9 @@ /// interface ImportMetaEnv { - readonly VITE_API_BASE_URL?: string + readonly VITE_API_BASE_URL?: string; } interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file + readonly env: ImportMetaEnv; +} diff --git a/src/CommandRunner.Website.VueJs/tsconfig.app.json b/src/CommandRunner.Website.VueJs/tsconfig.app.json new file mode 100644 index 0000000..13fc41a --- /dev/null +++ b/src/CommandRunner.Website.VueJs/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/src/CommandRunner.Website.VueJs/tsconfig.app.tsbuildinfo b/src/CommandRunner.Website.VueJs/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..272e357 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/main.ts","./src/vite-env.d.ts","./src/composables/useKeyboardShortcuts.ts","./src/composables/useOutputFormatter.ts","./src/composables/useTheme.ts","./src/services/api/client.ts","./src/services/api/commands.ts","./src/services/api/directories.ts","./src/services/api/index.ts","./src/services/api/profiles.ts","./src/services/api/types.ts","./src/stores/app.ts","./src/App.vue","./src/components/AlertBanner.vue","./src/components/AppIcon.vue","./src/components/CommandCard.vue","./src/components/CommandControls.vue","./src/components/CommandDetails.vue","./src/components/CommandForm.vue","./src/components/CommandPreview.vue","./src/components/CommandSelector.vue","./src/components/DirectorySelector.vue","./src/components/FormattedOutput.vue","./src/components/HeaderButtons.vue","./src/components/OutputPanel.vue","./src/components/ProfileCard.vue","./src/components/ProfileForm.vue","./src/components/ProfileSelector.vue","./src/components/SettingsDialog.vue","./src/components/ToastNotification.vue","./src/components/ToggleSwitch.vue"],"version":"5.6.3"} \ No newline at end of file diff --git a/src/CommandRunner.Website.VueJs/tsconfig.json b/src/CommandRunner.Website.VueJs/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/src/CommandRunner.Website.VueJs/tsconfig.node.json b/src/CommandRunner.Website.VueJs/tsconfig.node.json new file mode 100644 index 0000000..2142b4f --- /dev/null +++ b/src/CommandRunner.Website.VueJs/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/CommandRunner.Website.VueJs/tsconfig.node.tsbuildinfo b/src/CommandRunner.Website.VueJs/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..75ea001 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/src/CommandRunner.Website.VueJs/vite.config.ts b/src/CommandRunner.Website.VueJs/vite.config.ts new file mode 100644 index 0000000..9b1ead1 --- /dev/null +++ b/src/CommandRunner.Website.VueJs/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 5174, + // Only relevant when the page is loaded from this dev server itself (the Photino desktop + // host's DEBUG build does exactly that, for hot reload) -- window.location.origin is then + // localhost:5174, so API calls need forwarding to reach CommandRunner.Desktop's fixed DEBUG + // port. Not used when Desktop serves the built Vue bundle itself (same-origin already). + proxy: { + '/api': 'http://127.0.0.1:5081', + }, + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + }, +}); diff --git a/test/CommandRunner.UnitTests/CommandExecutionServiceTests.cs b/test/CommandRunner.UnitTests/CommandExecutionServiceTests.cs index acd7a4e..9205543 100644 --- a/test/CommandRunner.UnitTests/CommandExecutionServiceTests.cs +++ b/test/CommandRunner.UnitTests/CommandExecutionServiceTests.cs @@ -1,6 +1,5 @@ -using CommandRunner.Business.Services; -using CommandRunner.Business.Models; -using CommandRunner.Data.Models; +using CommandRunner.Api.Features.Commands; +using CommandRunner.Api.Features.Profiles; using System.Collections.Concurrent; namespace CommandRunner.UnitTests; @@ -24,7 +23,7 @@ public async Task ExecuteCommandAsync_ValidEchoCommand_ReturnsSuccess() var command = new Command { Name = "Echo Test", - Executable = OperatingSystem.IsWindows() ? "echo" : "echo", + Executable = "echo", Arguments = "Hello World", Shell = OperatingSystem.IsWindows() ? "cmd" : "bash", WorkingDirectory = Directory.GetCurrentDirectory() @@ -77,7 +76,9 @@ public async Task ExecuteCommandAsync_CommandWithEnvironmentVariables_InheritsVa { Name = "Env Test", Executable = "echo", - Arguments = "test", + // Actually reads the variable back so the assertion proves the child process received + // it, not just that it survived a round-trip through the result DTO. + Arguments = OperatingSystem.IsWindows() ? "%TEST_VAR%" : "$TEST_VAR", Shell = OperatingSystem.IsWindows() ? "cmd" : "bash", WorkingDirectory = Directory.GetCurrentDirectory(), EnvironmentVariables = new Dictionary @@ -90,6 +91,8 @@ public async Task ExecuteCommandAsync_CommandWithEnvironmentVariables_InheritsVa Assert.Multiple(() => { + Assert.That(result.WasSuccessful, Is.True, $"Command failed with exit code {result.ExitCode}. Output: '{result.StandardOutput}', Error: '{result.StandardError}'"); + Assert.That(result.StandardOutput, Does.Contain("test_value")); Assert.That(result.EnvironmentVariables, Is.Not.Null); Assert.That(result.EnvironmentVariables.Count, Is.EqualTo(command.EnvironmentVariables.Count)); Assert.That(result.EnvironmentVariables["TEST_VAR"], Is.EqualTo("test_value")); @@ -142,18 +145,17 @@ public async Task ExecuteCommandsAsync_MultipleCommands_ExecutesSequentially() }; var results = new List(); - var executionResults = await _executionService.ExecuteCommandsAsync( + var executionResults = (await _executionService.ExecuteCommandsAsync( commands, Directory.GetCurrentDirectory(), - new Progress(result => results.Add(result))); + new Progress(result => results.Add(result)))).ToList(); Assert.Multiple(() => { - Assert.That(executionResults.Count(), Is.EqualTo(2)); - Assert.That(results.Count, Is.EqualTo(2)); - Assert.That(results.All(r => r.WasSuccessful), Is.True); - Assert.That(results[0].CommandId, Is.EqualTo(commands[0].Id)); - Assert.That(results[1].CommandId, Is.EqualTo(commands[1].Id)); + Assert.That(executionResults, Has.Count.EqualTo(2)); + Assert.That(executionResults.All(r => r.WasSuccessful), Is.True); + Assert.That(executionResults[0].CommandId, Is.EqualTo(commands[0].Id)); + Assert.That(executionResults[1].CommandId, Is.EqualTo(commands[1].Id)); }); } @@ -181,17 +183,16 @@ public async Task ExecuteCommandsParallelAsync_MultipleCommands_ExecutesInParall }; var results = new ConcurrentBag(); - var executionResults = await _executionService.ExecuteCommandsParallelAsync( + var executionResults = (await _executionService.ExecuteCommandsParallelAsync( commands, Directory.GetCurrentDirectory(), maxParallelism: 2, - new Progress(result => results.Add(result))); + new Progress(result => results.Add(result)))).ToList(); Assert.Multiple(() => { - Assert.That(executionResults.Count(), Is.EqualTo(2)); - Assert.That(results.Count, Is.EqualTo(2)); - Assert.That(results.All(r => r.WasSuccessful), Is.True); + Assert.That(executionResults, Has.Count.EqualTo(2)); + Assert.That(executionResults.All(r => r.WasSuccessful), Is.True); }); } } diff --git a/test/CommandRunner.UnitTests/CommandRunner.UnitTests.csproj b/test/CommandRunner.UnitTests/CommandRunner.UnitTests.csproj index 2fa0cb5..238469b 100644 --- a/test/CommandRunner.UnitTests/CommandRunner.UnitTests.csproj +++ b/test/CommandRunner.UnitTests/CommandRunner.UnitTests.csproj @@ -22,7 +22,7 @@ - + diff --git a/test/CommandRunner.UnitTests/CommandValidationServiceTests.cs b/test/CommandRunner.UnitTests/CommandValidationServiceTests.cs index 95a8c0f..6b6e5c7 100644 --- a/test/CommandRunner.UnitTests/CommandValidationServiceTests.cs +++ b/test/CommandRunner.UnitTests/CommandValidationServiceTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; -using CommandRunner.Business.Services; -using CommandRunner.Business.Models; -using CommandRunner.Data.Models; +using CommandRunner.Api.Features.Commands; +using CommandRunner.Api.Features.Profiles; namespace CommandRunner.UnitTests; @@ -22,7 +21,7 @@ public async Task ValidateCommandAsync_ValidCommand_ReturnsSuccess() var command = new Command { Name = "Test Command", - Executable = "echo", + Executable = OperatingSystem.IsWindows() ? "cmd.exe" : "echo", Arguments = "Hello World", WorkingDirectory = Directory.GetCurrentDirectory() }; @@ -96,7 +95,7 @@ public async Task ValidateCommandAsync_CommandWithManyEnvironmentVariables_AddsW var command = new Command { Name = "Test Command", - Executable = "echo", + Executable = OperatingSystem.IsWindows() ? "cmd.exe" : "echo", Arguments = "test", WorkingDirectory = Directory.GetCurrentDirectory(), EnvironmentVariables = Enumerable.Range(1, 25) @@ -115,7 +114,7 @@ public async Task ValidateCommandAsync_IterationEnabledWithoutArguments_AddsWarn var command = new Command { Name = "Test Command", - Executable = "echo", + Executable = OperatingSystem.IsWindows() ? "cmd.exe" : "echo", Arguments = "", WorkingDirectory = Directory.GetCurrentDirectory(), IterationEnabled = true diff --git a/test/CommandRunner.UnitTests/IterationServiceTests.cs b/test/CommandRunner.UnitTests/IterationServiceTests.cs index f619b0d..8e2f8e8 100644 --- a/test/CommandRunner.UnitTests/IterationServiceTests.cs +++ b/test/CommandRunner.UnitTests/IterationServiceTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; -using CommandRunner.Business.Services; -using CommandRunner.Business.Models; -using CommandRunner.Data.Models; +using CommandRunner.Api.Features.Commands; +using CommandRunner.Api.Features.Profiles; namespace CommandRunner.UnitTests; diff --git a/test/CommandRunner.UnitTests/SecurityServiceTests.cs b/test/CommandRunner.UnitTests/SecurityServiceTests.cs index e42ac37..aeaaea1 100644 --- a/test/CommandRunner.UnitTests/SecurityServiceTests.cs +++ b/test/CommandRunner.UnitTests/SecurityServiceTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; -using CommandRunner.Business.Services; -using CommandRunner.Business.Models; -using CommandRunner.Data.Models; +using CommandRunner.Api.Features.Commands; +using CommandRunner.Api.Features.Profiles; namespace CommandRunner.UnitTests;