diff --git a/.env.desktop.example b/.env.desktop.example new file mode 100644 index 000000000..ce8539a42 --- /dev/null +++ b/.env.desktop.example @@ -0,0 +1,38 @@ +APP_NAME=Vito +APP_ENV=production +APP_KEY= +APP_DEBUG=false +APP_URL=http://127.0.0.1 + +VITO_DESKTOP=true +VITO_DATA_PATH= +VITO_ENV_PATH= +VITO_STORAGE_PATH= + +APP_SERVICES_CACHE= +APP_PACKAGES_CACHE= +APP_CONFIG_CACHE= +APP_ROUTES_CACHE= +APP_EVENTS_CACHE= +VIEW_COMPILED_PATH= + +DB_CONNECTION=sqlite +DB_DATABASE=database.sqlite + +QUEUE_CONNECTION=database +DB_QUEUE=default +QUEUE_FAILED_DRIVER=database-uuids + +CACHE_DRIVER=file +SESSION_DRIVER=file +FILESYSTEM_DISK=local +INERTIA_ENSURE_PAGES_EXIST=false + +MAIL_MAILER=log + +WS_HOST=127.0.0.1 +WS_PORT=8085 +WS_ALLOWED_ORIGINS= +WS_BROADCAST_SECRET= + +SCRIBE_AUTH_KEY="YOUR-API-KEY" diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml new file mode 100644 index 000000000..b4d882a85 --- /dev/null +++ b/.github/workflows/desktop-ci.yml @@ -0,0 +1,86 @@ +name: desktop-ci + +on: + pull_request: + paths: + - 'desktop/**' + - 'scripts/desktop/**' + - '.github/workflows/desktop-ci.yml' + - '.github/workflows/desktop-release.yml' + - 'vite.config.ts' + - 'bootstrap/app.php' + - 'config/desktop.php' + - 'app/Support/DesktopRuntime.php' + +jobs: + typecheck: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install desktop shell dependencies + run: npm ci + working-directory: desktop + + - name: Build desktop shell + run: npm run build + working-directory: desktop + + package-smoke: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: ctype, curl, dom, fileinfo, filter, ftp, iconv, intl, mbstring, openssl, pdo_sqlite, sqlite3, zip + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-progress --no-dev --optimize-autoloader + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install NPM dependencies + run: npm install --force + + - name: Build desktop frontend assets + run: npm run build:desktop + + - name: Assemble desktop app distribution + run: bash scripts/desktop/build-app.sh + + - name: Cache PHP runtime + uses: actions/cache@v4 + with: + path: desktop/resources/bin + key: desktop-php-linux-x64-${{ hashFiles('scripts/desktop/fetch-php.sh') }} + + - name: Fetch PHP runtime + run: bash scripts/desktop/fetch-php.sh linux x64 + + - name: Smoke test desktop app distribution + run: bash scripts/desktop/smoke-test.sh "$PWD/desktop/resources/bin/linux/x64/php" + + - name: Install desktop shell dependencies + run: npm ci + working-directory: desktop + + - name: Build desktop shell + run: npm run build + working-directory: desktop + + - name: Package desktop app (unpacked) + run: npx electron-builder --config electron-builder.yml --linux --x64 --dir --publish never + working-directory: desktop diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 000000000..a97f0c53f --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,161 @@ +name: desktop-release + +on: + workflow_dispatch: + inputs: + version: + description: 'Existing release tag to build and attach desktop apps to (e.g. 4.1.0)' + required: true + workflow_call: + inputs: + version: + description: 'Existing release tag to build and attach desktop apps to' + required: true + type: string + secrets: + DESKTOP_MAC_CSC_LINK: + required: false + DESKTOP_MAC_CSC_KEY_PASSWORD: + required: false + DESKTOP_WIN_CSC_LINK: + required: false + DESKTOP_WIN_CSC_KEY_PASSWORD: + required: false + DESKTOP_APPLE_ID: + required: false + DESKTOP_APPLE_APP_SPECIFIC_PASSWORD: + required: false + DESKTOP_APPLE_TEAM_ID: + required: false + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.name }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - name: macos-arm64 + os: macos-15 + platform: darwin + arch: arm64 + build-args: --mac --arm64 + - name: macos-x64 + os: macos-15-intel + platform: darwin + arch: x64 + build-args: --mac --x64 + - name: windows-x64 + os: windows-latest + platform: win32 + arch: x64 + build-args: --win --x64 + - name: linux-x64 + os: ubuntu-24.04 + platform: linux + arch: x64 + build-args: --linux --x64 + + steps: + - name: Remove optional "v" prefix + id: version + shell: bash + run: echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT" + env: + VERSION: ${{ inputs.version }} + + - name: Checkout release tag + uses: actions/checkout@v4 + with: + ref: ${{ steps.version.outputs.version }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: ctype, curl, dom, fileinfo, filter, ftp, iconv, intl, mbstring, openssl, pdo_sqlite, sqlite3, zip + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-progress --no-dev --optimize-autoloader + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install NPM dependencies + run: npm install --force + + - name: Build desktop frontend assets + run: npm run build:desktop + + - name: Assemble desktop app distribution + shell: bash + run: bash scripts/desktop/build-app.sh + + - name: Cache PHP runtime + uses: actions/cache@v4 + with: + path: desktop/resources/bin + key: desktop-php-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('scripts/desktop/fetch-php.sh') }} + + - name: Fetch PHP runtime + shell: bash + run: bash scripts/desktop/fetch-php.sh "${{ matrix.platform }}" "${{ matrix.arch }}" + + - name: Smoke test desktop app distribution + shell: bash + run: | + PHP_BIN="desktop/resources/bin/${{ matrix.platform }}/${{ matrix.arch }}/php" + if [ "${{ matrix.platform }}" = "win32" ]; then + PHP_BIN="$PHP_BIN.exe" + fi + bash scripts/desktop/smoke-test.sh "$PWD/$PHP_BIN" + + - name: Install desktop shell dependencies + shell: bash + run: npm ci + working-directory: desktop + + - name: Build desktop shell + shell: bash + run: npm run build + working-directory: desktop + + - name: Set desktop app version + shell: bash + run: npm version "${{ steps.version.outputs.version }}" --no-git-tag-version --allow-same-version + working-directory: desktop + + - name: Package desktop app + shell: bash + run: npx electron-builder --config electron-builder.yml ${{ matrix.build-args }} --publish never + working-directory: desktop + env: + CSC_LINK: ${{ secrets.DESKTOP_MAC_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_MAC_CSC_KEY_PASSWORD }} + WIN_CSC_LINK: ${{ secrets.DESKTOP_WIN_CSC_LINK }} + WIN_CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_WIN_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.DESKTOP_APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.DESKTOP_APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.DESKTOP_APPLE_TEAM_ID }} + CSC_IDENTITY_AUTO_DISCOVERY: ${{ secrets.DESKTOP_MAC_CSC_LINK != '' }} + + - name: Upload desktop apps to release + shell: bash + run: | + shopt -s nullglob + FILES=(desktop/release/*.dmg desktop/release/*-mac-*.zip desktop/release/*.exe desktop/release/*.AppImage) + if [ ${#FILES[@]} -eq 0 ]; then + echo "No desktop artifacts found to upload" >&2 + exit 1 + fi + gh release upload "$VERSION" "${FILES[@]}" --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 13d55fca5..54936618c 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -179,6 +179,22 @@ jobs: echo "Discord notification failed; release succeeded regardless" fi + desktop: + needs: release + permissions: + contents: write + uses: ./.github/workflows/desktop-release.yml + with: + version: ${{ needs.release.outputs.version }} + secrets: + DESKTOP_MAC_CSC_LINK: ${{ secrets.DESKTOP_MAC_CSC_LINK }} + DESKTOP_MAC_CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_MAC_CSC_KEY_PASSWORD }} + DESKTOP_WIN_CSC_LINK: ${{ secrets.DESKTOP_WIN_CSC_LINK }} + DESKTOP_WIN_CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_WIN_CSC_KEY_PASSWORD }} + DESKTOP_APPLE_ID: ${{ secrets.DESKTOP_APPLE_ID }} + DESKTOP_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.DESKTOP_APPLE_APP_SPECIFIC_PASSWORD }} + DESKTOP_APPLE_TEAM_ID: ${{ secrets.DESKTOP_APPLE_TEAM_ID }} + docker-tags: runs-on: ubuntu-24.04 needs: release diff --git a/.gitignore b/.gitignore index 4149fe675..547889349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ /.phpunit.cache /node_modules +/desktop/node_modules +/desktop/dist +/desktop/release +/desktop/resources/bin +/desktop-build +/public/build-desktop /public/hot /public/storage /storage/*.key diff --git a/app/Actions/Desktop/SetupDesktopAdmin.php b/app/Actions/Desktop/SetupDesktopAdmin.php new file mode 100644 index 000000000..45aef6061 --- /dev/null +++ b/app/Actions/Desktop/SetupDesktopAdmin.php @@ -0,0 +1,35 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'password' => ['required', 'string', 'confirmed'], + ])->validate(); + + return DB::transaction(function () use ($input): User { + $user = app(CreateUser::class)->create([ + 'name' => $input['name'] ?? null, + 'email' => $input['email'] ?? null, + 'password' => $input['password'] ?? null, + 'role' => UserRole::ADMIN->value, + ]); + + $user->ensureHasDefaultProject(); + + return $user; + }); + } +} diff --git a/app/Actions/Desktop/VerifyDesktopUnlock.php b/app/Actions/Desktop/VerifyDesktopUnlock.php new file mode 100644 index 000000000..bc1f8fa3f --- /dev/null +++ b/app/Actions/Desktop/VerifyDesktopUnlock.php @@ -0,0 +1,54 @@ + $input + */ + public function verify(User $user, array $input): void + { + $validated = Validator::make($input, [ + 'password' => ['required', 'string'], + 'code' => ['nullable', 'string'], + ])->validate(); + + if (! Hash::check($validated['password'], $user->password)) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + if ($user->hasEnabledTwoFactorAuthentication()) { + $this->verifyTwoFactorCode($user, (string) ($validated['code'] ?? '')); + } + } + + private function verifyTwoFactorCode(User $user, string $code): void + { + if ($code !== '') { + if (app(TwoFactorAuthenticationProvider::class)->verify(decrypt($user->two_factor_secret), $code)) { + return; + } + + foreach ($user->recoveryCodes() as $recoveryCode) { + if (hash_equals($recoveryCode, $code)) { + $user->replaceRecoveryCode($code); + + return; + } + } + } + + throw ValidationException::withMessages([ + 'code' => __('The provided two factor authentication code was invalid.'), + ]); + } +} diff --git a/app/Console/Commands/GenerateKeysCommand.php b/app/Console/Commands/GenerateKeysCommand.php index 64187f5a8..47145bf13 100644 --- a/app/Console/Commands/GenerateKeysCommand.php +++ b/app/Console/Commands/GenerateKeysCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\File; +use phpseclib3\Crypt\RSA; class GenerateKeysCommand extends Command { @@ -22,11 +23,13 @@ public function handle(): void return; } - exec('openssl genpkey -algorithm RSA -out '.$privateKeyPath); - exec('chmod 600 '.$privateKeyPath); - exec('ssh-keygen -y -f '.$privateKeyPath.' > '.$publicKeyPath); - exec('chown -R '.get_current_user().':'.get_current_user().' '.$privateKeyPath); - exec('chown -R '.get_current_user().':'.get_current_user().' '.$publicKeyPath); + $privateKey = RSA::createKey(2048); + + File::put($privateKeyPath, $privateKey->toString('PKCS8')); + chmod($privateKeyPath, 0600); + + File::put($publicKeyPath, $privateKey->getPublicKey()->toString('OpenSSH', ['comment' => 'vito'])); + chmod($publicKeyPath, 0644); $this->info('Keys generated successfully.'); } diff --git a/app/Http/Controllers/ConsoleController.php b/app/Http/Controllers/ConsoleController.php index ec6cfb775..6a64500d5 100644 --- a/app/Http/Controllers/ConsoleController.php +++ b/app/Http/Controllers/ConsoleController.php @@ -5,6 +5,7 @@ use App\Actions\WebSockets\GenerateWebSocketToken; use App\Http\Resources\ServerResource; use App\Models\Server; +use App\Support\DesktopRuntime; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -48,19 +49,7 @@ public function token(Server $server, Request $request): JsonResponse 'ssh_user' => $request->input('user'), ]); - $appUrl = parse_url(config('app.ws_url') ?: config('app.url')); - $isSecure = ($appUrl['scheme'] ?? 'http') === 'https'; - $wsProtocol = $isSecure ? 'wss' : 'ws'; - $host = $appUrl['host'] ?? 'localhost'; - $port = $appUrl['port'] ?? ($isSecure ? 443 : 80); - - if (app()->environment('local') && ! config('app.ws_url')) { - $wsPort = config('core.ws_port', 8085); - $result['url'] = "{$wsProtocol}://{$host}:{$wsPort}/ws/terminal"; - } else { - $portSuffix = (($isSecure && $port == 443) || (! $isSecure && $port == 80)) ? '' : ":{$port}"; - $result['url'] = "{$wsProtocol}://{$host}{$portSuffix}/ws/terminal"; - } + $result['url'] = DesktopRuntime::websocketUrl('/ws/terminal'); return response()->json($result); } diff --git a/app/Http/Controllers/DesktopAuthController.php b/app/Http/Controllers/DesktopAuthController.php new file mode 100644 index 000000000..a0e08564a --- /dev/null +++ b/app/Http/Controllers/DesktopAuthController.php @@ -0,0 +1,106 @@ +lockedUserId($request); + + $users = User::query()->orderBy('name')->get(); + + return Inertia::render('auth/desktop-login', [ + 'users' => UserResource::collection($users), + 'setup_required' => $users->isEmpty(), + 'locked' => $lockedUserId !== null, + 'locked_user_id' => $lockedUserId, + ]); + } + + #[Post('/setup', name: 'desktop.setup', middleware: ['guest', 'throttle:desktop-auth'])] + public function setup(Request $request, SetupDesktopAdmin $setupDesktopAdmin): RedirectResponse + { + abort_unless(User::query()->doesntExist(), 404); + + $user = $setupDesktopAdmin->create($request->all()); + + Auth::login($user); + + $request->session()->forget(self::LOCKED_USER_ID); + $request->session()->regenerate(); + + return redirect()->intended(RouteServiceProvider::HOME); + } + + #[Post('/login/{user}', name: 'desktop.login.store', middleware: ['guest', 'throttle:desktop-auth'])] + public function store(Request $request, User $user, VerifyDesktopUnlock $verifyDesktopUnlock): RedirectResponse + { + if ($this->lockedUserId($request) !== null) { + $verifyDesktopUnlock->verify($user, $request->all()); + } + + Auth::login($user); + + $request->session()->forget(self::LOCKED_USER_ID); + $request->session()->regenerate(); + + $user->ensureHasDefaultProject(); + + return redirect()->intended(RouteServiceProvider::HOME); + } + + #[Post('/lock', name: 'desktop.lock', middleware: 'auth')] + public function lock(Request $request): RedirectResponse + { + /** @var User $user */ + $user = $request->user(); + + Auth::guard('web')->logout(); + + $request->session()->migrate(true); + $request->session()->put(self::LOCKED_USER_ID, $user->id); + $request->session()->regenerateToken(); + + return to_route('desktop.login'); + } + + private function lockedUserId(Request $request): ?int + { + $lockedUserId = $request->session()->get(self::LOCKED_USER_ID); + + if (! is_numeric($lockedUserId)) { + return null; + } + + $lockedUserId = (int) $lockedUserId; + + if (! User::query()->whereKey($lockedUserId)->exists()) { + $request->session()->forget(self::LOCKED_USER_ID); + + return null; + } + + return $lockedUserId; + } +} diff --git a/app/Http/Controllers/EventsController.php b/app/Http/Controllers/EventsController.php index c0cd6f4d0..2cc383ca4 100644 --- a/app/Http/Controllers/EventsController.php +++ b/app/Http/Controllers/EventsController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Actions\WebSockets\GenerateWebSocketToken; +use App\Support\DesktopRuntime; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Spatie\RouteAttributes\Attributes\Middleware; @@ -19,19 +20,7 @@ public function token(Request $request): JsonResponse 'project_id' => $request->user()->current_project_id, ]); - $appUrl = parse_url(config('app.ws_url') ?: config('app.url')); - $isSecure = ($appUrl['scheme'] ?? 'http') === 'https'; - $wsProtocol = $isSecure ? 'wss' : 'ws'; - $host = $appUrl['host'] ?? 'localhost'; - $port = $appUrl['port'] ?? ($isSecure ? 443 : 80); - - if (app()->environment('local') && ! config('app.ws_url')) { - $wsPort = config('core.ws_port', 8085); - $result['url'] = "{$wsProtocol}://{$host}:{$wsPort}/ws/events"; - } else { - $portSuffix = (($isSecure && $port == 443) || (! $isSecure && $port == 80)) ? '' : ":{$port}"; - $result['url'] = "{$wsProtocol}://{$host}{$portSuffix}/ws/events"; - } + $result['url'] = DesktopRuntime::websocketUrl('/ws/events'); return response()->json($result); } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 97ad73c9b..9570aecd3 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Support\DesktopRuntime; use Illuminate\Http\RedirectResponse; use Spatie\RouteAttributes\Attributes\Get; @@ -14,6 +15,6 @@ public function __invoke(): RedirectResponse return redirect()->route('servers'); } - return redirect()->route('login'); + return redirect()->route(DesktopRuntime::enabled() ? 'desktop.login' : 'login'); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 6465107fa..d281d55a9 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -5,6 +5,7 @@ use App\Http\Middleware\Authenticate; use App\Http\Middleware\CanSeeProjectMiddleware; use App\Http\Middleware\EncryptCookies; +use App\Http\Middleware\EnsureDesktopMode; use App\Http\Middleware\HandleAppearance; use App\Http\Middleware\HandleInertiaRequests; use App\Http\Middleware\HasProjectMiddleware; @@ -98,5 +99,6 @@ class Kernel extends HttpKernel 'has-project' => HasProjectMiddleware::class, 'can-see-project' => CanSeeProjectMiddleware::class, 'must-be-admin' => MustBeAdminMiddleware::class, + 'desktop' => EnsureDesktopMode::class, ]; } diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index aa87d364e..0b7397f49 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -2,6 +2,7 @@ namespace App\Http\Middleware; +use App\Support\DesktopRuntime; use Illuminate\Auth\Middleware\Authenticate as Middleware; use Illuminate\Http\Request; @@ -12,6 +13,10 @@ class Authenticate extends Middleware */ protected function redirectTo(Request $request): ?string { - return $request->expectsJson() ? null : url('/'); + if ($request->expectsJson()) { + return null; + } + + return route(DesktopRuntime::enabled() ? 'desktop.login' : 'login'); } } diff --git a/app/Http/Middleware/EnsureDesktopMode.php b/app/Http/Middleware/EnsureDesktopMode.php new file mode 100644 index 000000000..db247107d --- /dev/null +++ b/app/Http/Middleware/EnsureDesktopMode.php @@ -0,0 +1,18 @@ + config('app.env'), 'demo' => config('app.demo'), 'quote' => ['message' => trim($message), 'author' => trim($author)], + 'desktop' => [ + 'enabled' => DesktopRuntime::enabled(), + ], 'auth' => $user ? [ 'user' => UserResource::make($user->load('projects')), 'currentProject' => ProjectResource::make($currentProject), diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 419d9be3c..195f5dcd6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,9 +13,11 @@ use App\Listeners\HandleSiteDeletedStats; use App\Listeners\SocketEventListener; use App\Models\PersonalAccessToken; +use App\Support\DesktopRuntime; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Facades\Vite; use Illuminate\Support\ServiceProvider; use Laravel\Sanctum\Sanctum; @@ -30,6 +32,10 @@ public function boot(): void { ResourceCollection::withoutWrapping(); + if (DesktopRuntime::enabled()) { + Vite::useBuildDirectory((string) config('desktop.build_directory')); + } + // facades $this->app->bind('ssh', fn (): SSH => new SSH); $this->app->bind('notifier', fn (): Notifier => new Notifier); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 8bbdc4ea2..2fb9c07ab 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -5,6 +5,9 @@ use App\Actions\User\ResetUserPassword; use App\Actions\User\UpdateUserPassword; use App\Actions\User\UpdateUserProfileInformation; +use App\Models\User; +use App\Support\DesktopRuntime; +use Closure; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; @@ -25,14 +28,14 @@ public function boot(): void Fortify::resetUserPasswordsUsing(ResetUserPassword::class); Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class); - Fortify::loginView(fn () => Inertia::render('auth/login')); - Fortify::requestPasswordResetLinkView(fn () => Inertia::render('auth/forgot-password')); - Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password')); - Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/reset-password', [ + Fortify::loginView($this->desktopAware(fn () => Inertia::render('auth/login'))); + Fortify::requestPasswordResetLinkView($this->desktopAware(fn () => Inertia::render('auth/forgot-password'))); + Fortify::confirmPasswordView($this->desktopAware(fn () => Inertia::render('auth/confirm-password'))); + Fortify::resetPasswordView($this->desktopAware(fn (Request $request) => Inertia::render('auth/reset-password', [ 'email' => $request->email, 'token' => $request->route('token'), - ])); - Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor')); + ]))); + Fortify::twoFactorChallengeView($this->desktopAware(fn () => Inertia::render('auth/two-factor'))); RateLimiter::for('login', function (Request $request) { $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username()).'|'.$request->ip())); @@ -43,5 +46,17 @@ public function boot(): void RateLimiter::for('two-factor', function (Request $request) { return Limit::perMinute(5)->by($request->session()->get('login.id')); }); + + RateLimiter::for('desktop-auth', function (Request $request) { + $user = $request->route('user'); + $userKey = $user instanceof User ? $user->id : (string) $user; + + return Limit::perMinute(5)->by($userKey.'|'.$request->ip()); + }); + } + + private function desktopAware(Closure $view): Closure + { + return fn (Request $request) => DesktopRuntime::enabled() ? to_route('desktop.login') : $view($request); } } diff --git a/app/Support/DesktopRuntime.php b/app/Support/DesktopRuntime.php new file mode 100644 index 000000000..ea58b256b --- /dev/null +++ b/app/Support/DesktopRuntime.php @@ -0,0 +1,76 @@ +environment('local') && ! config('app.ws_url')) || self::enabled()) { + return "{$wsProtocol}://{$host}:".self::websocketPort().$path; + } + + $portSuffix = (($isSecure && $port == 443) || (! $isSecure && $port == 80)) ? '' : ":{$port}"; + + return "{$wsProtocol}://{$host}{$portSuffix}{$path}"; + } + + private static function joinPath(string $basePath, string $path): string + { + if ($path === '') { + return $basePath; + } + + return rtrim($basePath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR); + } +} diff --git a/app/Support/helpers.php b/app/Support/helpers.php index 56debe476..e996b73bb 100755 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -14,12 +14,12 @@ function generate_public_key(string $privateKeyPath, string $publicKeyPath): void { chmod($privateKeyPath, 0400); - exec("ssh-keygen -y -f {$privateKeyPath} > {$publicKeyPath}"); + exec('ssh-keygen -y -f '.escapeshellarg($privateKeyPath).' > '.escapeshellarg($publicKeyPath)); } function generate_key_pair(string $path): void { - exec("ssh-keygen -t ed25519 -m PEM -N '' -f {$path}"); + exec('ssh-keygen -t ed25519 -m PEM -N \'\' -f '.escapeshellarg($path)); chmod($path, 0400); } diff --git a/bootstrap/app.php b/bootstrap/app.php index b4a5fc363..a231f2620 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -20,6 +20,22 @@ $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); +$envValue = static function (string $key): ?string { + $value = $_ENV[$key] ?? getenv($key); + + return is_string($value) && $value !== '' ? $value : null; +}; + +if (filter_var($envValue('VITO_DESKTOP') ?? false, FILTER_VALIDATE_BOOL)) { + if (($environmentPath = $envValue('VITO_ENV_PATH')) !== null) { + $app->useEnvironmentPath($environmentPath); + } + + if (($storagePath = $envValue('VITO_STORAGE_PATH')) !== null) { + $app->useStoragePath($storagePath); + } +} + /* |-------------------------------------------------------------------------- | Bind Important Interfaces diff --git a/config/desktop.php b/config/desktop.php new file mode 100644 index 000000000..b74ac92d2 --- /dev/null +++ b/config/desktop.php @@ -0,0 +1,9 @@ + filter_var(env('VITO_DESKTOP', false), FILTER_VALIDATE_BOOL), + 'data_path' => env('VITO_DATA_PATH'), + 'env_path' => env('VITO_ENV_PATH'), + 'storage_path' => env('VITO_STORAGE_PATH'), + 'build_directory' => 'build-desktop', +]; diff --git a/config/inertia.php b/config/inertia.php index 450eec212..10ab11078 100644 --- a/config/inertia.php +++ b/config/inertia.php @@ -39,7 +39,7 @@ */ 'pages' => [ - 'ensure_pages_exist' => true, + 'ensure_pages_exist' => (bool) env('INERTIA_ENSURE_PAGES_EXIST', true), 'paths' => [ resource_path('js/pages'), diff --git a/config/queue.php b/config/queue.php index 89056c241..fdb353b31 100755 --- a/config/queue.php +++ b/config/queue.php @@ -53,6 +53,15 @@ 'after_commit' => false, ], + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'jobs', + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => max(300, (int) env('BACKUP_RUN_TIMEOUT', 3600)) + 60, + 'after_commit' => false, + ], + 'ssh' => [ 'driver' => 'redis', 'connection' => 'default', diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 000000000..e0b3eca50 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,68 @@ +# Vito Desktop + +The desktop shell runs the Laravel app as a local backend and opens the existing Inertia UI in an Electron window. Everything ships in a single installer: the Electron shell, the Laravel application, the built frontend assets, and a portable PHP 8.4 runtime. No PHP, Redis, Docker, or web server is required on the user's machine. + +## Architecture + +The Electron main process supervises four backend processes, all bound to `127.0.0.1` on dynamically allocated ports: + +| Process | Command | +| --- | --- | +| `vito-http` | `php -S 127.0.0.1: -t public ` | +| `vito-queue` | `php artisan queue:work database --queue=default,ssh,ssh-certbot` | +| `vito-scheduler` | `php artisan schedule:work` | +| `vito-websocket` | `php artisan ws:serve --host=127.0.0.1 --port=` | + +Desktop mode uses SQLite, database queues, and file cache/sessions. All mutable data lives outside the app bundle: + +- macOS: `~/Library/Application Support/Vito` +- Windows: `%APPDATA%/Vito` +- Linux: `~/.config/Vito` + +## Development + +```sh +npm install --prefix desktop +npm run dev # or: npm run build:desktop (desktop assets) +npm run desktop:dev +``` + +The dev shell uses the repository root as the Laravel app path and the system PHP binary unless `PHP_BINARY` is set. Frontend assets come from the Vite dev server (`public/hot`) or from `public/build-desktop` (`npm run build:desktop`), never from the committed `public/build`. + +## Packaging locally + +```sh +composer install --no-dev --optimize-autoloader +npm install +npm run build:desktop +bash scripts/desktop/build-app.sh +bash scripts/desktop/fetch-php.sh darwin arm64 # platform: darwin|linux|win32, arch: arm64|x64 +bash scripts/desktop/smoke-test.sh "$PWD/desktop/resources/bin/darwin/arm64/php" +npm --prefix desktop ci +npm run desktop:build +npm run desktop:package -- --mac --arm64 # --win --x64 / --linux --x64 +``` + +Installers land in `desktop/release/`. + +`fetch-php.sh` builds a static PHP with [static-php-cli](https://github.com/crazywhalecc/static-php-cli) on macOS/Linux (slow the first time, cached afterwards) and downloads the official php.net build on Windows. + +## Releasing + +The `releases` workflow builds and attaches desktop installers (macOS arm64/x64 dmg+zip, Windows x64 NSIS installer, Linux x86_64 AppImage) to every GitHub release automatically. The `desktop-release` workflow can also be dispatched on its own with an existing release tag to (re)build and upload the desktop apps for that release. + +### Code signing secrets (all optional) + +Without these secrets the workflows produce unsigned builds (macOS users must right-click → Open on first launch). + +| Secret | Purpose | +| --- | --- | +| `DESKTOP_MAC_CSC_LINK` | Developer ID Application certificate (.p12, base64) | +| `DESKTOP_MAC_CSC_KEY_PASSWORD` | Password for the .p12 | +| `DESKTOP_APPLE_ID` | Apple ID for notarization | +| `DESKTOP_APPLE_APP_SPECIFIC_PASSWORD` | App-specific password for notarization | +| `DESKTOP_APPLE_TEAM_ID` | Apple team ID for notarization | +| `DESKTOP_WIN_CSC_LINK` | Windows code signing certificate (.pfx, base64) | +| `DESKTOP_WIN_CSC_KEY_PASSWORD` | Password for the .pfx | + +Notarization runs automatically when the three `DESKTOP_APPLE_*` secrets are present together with the macOS certificate. diff --git a/desktop/build/icon.png b/desktop/build/icon.png new file mode 100644 index 000000000..3035fd2f8 Binary files /dev/null and b/desktop/build/icon.png differ diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml new file mode 100644 index 000000000..0eaf701bb --- /dev/null +++ b/desktop/electron-builder.yml @@ -0,0 +1,43 @@ +appId: com.vitodeploy.vito +productName: Vito +artifactName: Vito-${version}-${os}-${arch}.${ext} +directories: + output: release +files: + - dist/**/* + - package.json +extraResources: + - from: ../desktop-build/app + to: app + filter: + - "**/*" + - from: resources/bin + to: bin + filter: + - "**/*" +mac: + category: public.app-category.developer-tools + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + binaries: + - "Resources/bin/darwin/${arch}/php" + target: + - dmg + - zip +dmg: + sign: false +win: + target: + - nsis +nsis: + oneClick: false + allowToChangeInstallationDirectory: true +linux: + category: Development + maintainer: VitoDeploy + target: + - AppImage +publish: null +npmRebuild: false diff --git a/desktop/entitlements.mac.plist b/desktop/entitlements.mac.plist new file mode 100644 index 000000000..645f4beff --- /dev/null +++ b/desktop/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 000000000..3f4b02318 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,3627 @@ +{ + "name": "@vitodeploy/vito-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vitodeploy/vito-desktop", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^26.1.0", + "electron": "^43.0.0", + "electron-builder": "^26.15.3", + "typescript": "^6.0.3" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.1.0.tgz", + "integrity": "sha512-GFky+1JeO8fpSqtZCh+2wFRN/MVbAhm7tXI2M7BA1Os79OR8LEoxRtAbRPyxvmKWhEMF/p10rNJkkgP1klI13g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/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/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.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/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-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/cross-spawn/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/cross-spawn/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/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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "43.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.0.0.tgz", + "integrity": "sha512-PV60GsWU6qufhuOhw3n+Yix3WPDcqDtBqE8orbEQGQGHEkgp9o/JCPgb7L4vIL0r1HnfPdqSRtboOTqbDkcFDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.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/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "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/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "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/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/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/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/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/desktop/package.json b/desktop/package.json new file mode 100644 index 000000000..3436918c4 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,19 @@ +{ + "name": "@vitodeploy/vito-desktop", + "productName": "Vito", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "dist/main.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "electron .", + "package": "electron-builder --config electron-builder.yml" + }, + "devDependencies": { + "@types/node": "^26.1.0", + "electron": "^43.0.0", + "electron-builder": "^26.15.3", + "typescript": "^6.0.3" + } +} diff --git a/desktop/src/env-file.ts b/desktop/src/env-file.ts new file mode 100644 index 000000000..858cbc46c --- /dev/null +++ b/desktop/src/env-file.ts @@ -0,0 +1,69 @@ +import { randomBytes } from 'node:crypto'; +import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; + +const SIMPLE_VALUE = /^[A-Za-z0-9_./:@-]*$/; + +export function readEnvFile(path: string): Record { + if (!existsSync(path)) { + return {}; + } + + const values: Record = {}; + const lines = readFileSync(path, 'utf8').split(/\r?\n/); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const equalsAt = trimmed.indexOf('='); + if (equalsAt === -1) { + continue; + } + + const key = trimmed.slice(0, equalsAt).trim(); + let value = trimmed.slice(equalsAt + 1).trim(); + + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1).replace(/\\(["\\])/g, '$1'); + } else if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + + values[key] = value; + } + + return values; +} + +export function writeEnvFile(path: string, values: Record): void { + const lines = Object.entries(values).map(([key, value]) => `${key}=${formatEnvValue(value)}`); + writeFileSync(path, `${lines.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 }); + + try { + chmodSync(path, 0o600); + } catch { + return; + } +} + +export function appKey(): string { + return `base64:${randomBytes(32).toString('base64')}`; +} + +export function secret(): string { + return randomBytes(32).toString('hex'); +} + +function formatEnvValue(value: string): string { + if (value === '') { + return ''; + } + + if (SIMPLE_VALUE.test(value)) { + return value; + } + + return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +} diff --git a/desktop/src/health-check.ts b/desktop/src/health-check.ts new file mode 100644 index 000000000..34bc672cd --- /dev/null +++ b/desktop/src/health-check.ts @@ -0,0 +1,24 @@ +export async function waitForHttp(url: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + + while (Date.now() < deadline) { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(3000) }); + if (response.ok) { + return; + } + lastError = new Error(`Health check failed with ${response.status}`); + } catch (error) { + lastError = error; + } + + await delay(500); + } + + throw lastError instanceof Error ? lastError : new Error('Health check timed out'); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/desktop/src/laravel-runtime.ts b/desktop/src/laravel-runtime.ts new file mode 100644 index 000000000..c9ac30ed4 --- /dev/null +++ b/desktop/src/laravel-runtime.ts @@ -0,0 +1,99 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { DesktopLog } from './logging.js'; +import { ProcessSupervisor, runCommand } from './process-supervisor.js'; +import { RuntimeContext } from './runtime-paths.js'; + +export async function prepareLaravelRuntime(context: RuntimeContext, log: DesktopLog): Promise { + await artisan(context, log, ['optimize:clear'], 'artisan:optimize-clear', 120000); + await artisan(context, log, ['migrate', '--force'], 'artisan:migrate', 120000); + await artisan(context, log, ['optimize'], 'artisan:optimize', 120000); + await ensureSshKeys(context, log); +} + +export function startLaravelProcesses(context: RuntimeContext, supervisor: ProcessSupervisor): void { + const common = { + cwd: context.appRoot, + env: context.env, + }; + + supervisor.start({ + name: 'vito-websocket', + command: context.phpBinary, + args: [...context.phpArgs, 'artisan', 'ws:serve', '--host=127.0.0.1', `--port=${context.websocketPort}`], + ...common, + }); + + supervisor.start({ + name: 'vito-queue', + command: context.phpBinary, + args: [ + ...context.phpArgs, + 'artisan', + 'queue:work', + 'database', + '--queue=default,ssh,ssh-certbot', + '--sleep=3', + '--timeout=3600', + '--tries=1', + ], + gracefulStop: { + command: context.phpBinary, + args: [...context.phpArgs, 'artisan', 'queue:restart'], + }, + ...common, + }); + + supervisor.start({ + name: 'vito-scheduler', + command: context.phpBinary, + args: [...context.phpArgs, 'artisan', 'schedule:work'], + ...common, + }); + + supervisor.start({ + name: 'vito-http', + command: context.phpBinary, + args: [ + ...context.phpArgs, + '-S', + `127.0.0.1:${context.httpPort}`, + join(context.appRoot, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'), + ], + maxRestarts: 3, + restartWindowMs: 60000, + cwd: join(context.appRoot, 'public'), + env: process.platform === 'win32' ? context.env : { ...context.env, PHP_CLI_SERVER_WORKERS: '8' }, + }); +} + +function artisan( + context: RuntimeContext, + log: DesktopLog, + args: string[], + name: string, + timeoutMs?: number, +): Promise { + return runCommand(context.phpBinary, [...context.phpArgs, 'artisan', ...args], { + cwd: context.appRoot, + env: context.env, + log, + name, + timeoutMs, + }); +} + +async function ensureSshKeys(context: RuntimeContext, log: DesktopLog): Promise { + const publicKey = join(context.storagePath, 'ssh-public.key'); + const privateKey = join(context.storagePath, 'ssh-private.pem'); + + if (existsSync(publicKey) && existsSync(privateKey)) { + return; + } + + await artisan(context, log, ['ssh-key:generate'], 'artisan:ssh-key-generate', 120000); + + if (!existsSync(publicKey) || !existsSync(privateKey)) { + throw new Error('SSH key generation completed without creating both required key files'); + } +} diff --git a/desktop/src/loading-page.ts b/desktop/src/loading-page.ts new file mode 100644 index 000000000..419a81ace --- /dev/null +++ b/desktop/src/loading-page.ts @@ -0,0 +1,63 @@ +export function loadingPage(message: string): string { + return dataPage('Starting Vito', message); +} + +export function errorPage(message: string): string { + return dataPage('Vito could not start', message); +} + +function dataPage(title: string, message: string): string { + const html = ` + + + + + ${escapeHtml(title)} + + + +
+

${escapeHtml(title)}

+

${escapeHtml(message)}

+
+ +`; + + return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/desktop/src/logging.ts b/desktop/src/logging.ts new file mode 100644 index 000000000..d3e6a69f2 --- /dev/null +++ b/desktop/src/logging.ts @@ -0,0 +1,41 @@ +import { appendFileSync, mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; + +export class DesktopLog { + constructor( + private readonly path: string, + private readonly redactions: string[] = [], + ) { + mkdirSync(dirname(path), { recursive: true }); + } + + info(message: string): void { + this.write('info', message); + } + + error(message: string): void { + this.write('error', message); + } + + line(source: string, message: string): void { + const redacted = this.redact(message); + const terminated = redacted.endsWith('\n') ? redacted : `${redacted}\n`; + appendFileSync(this.path, `[${new Date().toISOString()}] [${source}] ${terminated}`, 'utf8'); + } + + private write(level: 'info' | 'error', message: string): void { + appendFileSync(this.path, `[${new Date().toISOString()}] [desktop:${level}] ${this.redact(message)}\n`, 'utf8'); + } + + private redact(message: string): string { + let redacted = message; + + for (const secret of this.redactions) { + if (secret !== '') { + redacted = redacted.split(secret).join('[redacted]'); + } + } + + return redacted; + } +} diff --git a/desktop/src/main.ts b/desktop/src/main.ts new file mode 100644 index 000000000..cbf9c10da --- /dev/null +++ b/desktop/src/main.ts @@ -0,0 +1,149 @@ +import { app, BrowserWindow, dialog, shell } from 'electron'; +import { join } from 'node:path'; +import { waitForHttp } from './health-check.js'; +import { prepareLaravelRuntime, startLaravelProcesses } from './laravel-runtime.js'; +import { loadingPage, errorPage } from './loading-page.js'; +import { DesktopLog } from './logging.js'; +import { cleanupStaleProcesses, ProcessSupervisor } from './process-supervisor.js'; +import { createRuntimeContext } from './runtime-paths.js'; + +let mainWindow: BrowserWindow | null = null; +let supervisor: ProcessSupervisor | null = null; +let quitting = false; + +const gotLock = app.requestSingleInstanceLock(); + +if (!gotLock) { + app.exit(0); +} + +for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.on(signal, () => { + app.quit(); + }); +} + +app.on('second-instance', () => { + if (!mainWindow) { + return; + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + + mainWindow.focus(); +}); + +app.on('before-quit', async (event) => { + if (quitting) { + event.preventDefault(); + + return; + } + + if (!supervisor) { + return; + } + + event.preventDefault(); + quitting = true; + const currentSupervisor = supervisor; + supervisor = null; + await currentSupervisor.stopAll(); + app.exit(0); +}); + +app + .whenReady() + .then(async () => { + if (!gotLock) { + return; + } + + mainWindow = createWindow(); + await mainWindow.loadURL(loadingPage('Preparing local runtime...')); + + try { + const context = await createRuntimeContext(); + const log = new DesktopLog(context.logPath, context.redactions); + const pidFilePath = join(context.runtimePath, 'processes.json'); + + cleanupStaleProcesses(pidFilePath, log); + + supervisor = new ProcessSupervisor(log, pidFilePath, (name) => { + if (quitting || name !== 'vito-http') { + return; + } + + const message = 'The local Vito backend repeatedly crashed and could not be restarted.'; + log.error(message); + void mainWindow?.loadURL(errorPage(message)); + dialog.showErrorBox('Vito backend stopped', message); + }); + + log.info(`Using Laravel app path: ${context.appRoot}`); + log.info(`Using desktop data path: ${context.dataPath}`); + + attachNavigationGuards(mainWindow, context.appUrl); + + await mainWindow.loadURL(loadingPage('Migrating local database...')); + await prepareLaravelRuntime(context, log); + + await mainWindow.loadURL(loadingPage('Starting backend services...')); + startLaravelProcesses(context, supervisor); + await waitForHttp(`${context.appUrl}/api/health`, 45000); + + await mainWindow.loadURL(`${context.appUrl}/desktop/login`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await mainWindow.loadURL(errorPage(message)); + dialog.showErrorBox('Vito could not start', message); + } + }) + .catch((error) => { + dialog.showErrorBox('Vito could not start', error instanceof Error ? error.message : String(error)); + }); + +app.on('window-all-closed', () => { + app.quit(); +}); + +function createWindow(): BrowserWindow { + return new BrowserWindow({ + width: 1440, + height: 960, + minWidth: 1024, + minHeight: 700, + title: 'Vito', + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + preload: join(app.getAppPath(), 'dist', 'preload.js'), + sandbox: true, + }, + }); +} + +function attachNavigationGuards(window: BrowserWindow, appUrl: string): void { + const openExternally = (url: string): void => { + if (url.startsWith('http://') || url.startsWith('https://')) { + void shell.openExternal(url); + } + }; + + window.webContents.setWindowOpenHandler(({ url }) => { + openExternally(url); + + return { action: 'deny' }; + }); + + window.webContents.on('will-navigate', (event, url) => { + if (url === appUrl || url.startsWith(`${appUrl}/`)) { + return; + } + + event.preventDefault(); + openExternally(url); + }); +} diff --git a/desktop/src/ports.ts b/desktop/src/ports.ts new file mode 100644 index 000000000..f4dac0dd8 --- /dev/null +++ b/desktop/src/ports.ts @@ -0,0 +1,19 @@ +import { createServer } from 'node:net'; + +export function getAvailablePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + if (typeof address === 'string' || address === null) { + server.close(() => reject(new Error('Unable to allocate a TCP port'))); + return; + } + + const port = address.port; + server.close(() => resolve(port)); + }); + }); +} diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts new file mode 100644 index 000000000..55ce53009 --- /dev/null +++ b/desktop/src/preload.ts @@ -0,0 +1,5 @@ +import { contextBridge } from 'electron'; + +contextBridge.exposeInMainWorld('vitoDesktop', { + platform: process.platform, +}); diff --git a/desktop/src/process-supervisor.ts b/desktop/src/process-supervisor.ts new file mode 100644 index 000000000..d117d4346 --- /dev/null +++ b/desktop/src/process-supervisor.ts @@ -0,0 +1,353 @@ +import { ChildProcessWithoutNullStreams, execFileSync, spawn } from 'node:child_process'; +import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { basename } from 'node:path'; +import { DesktopLog } from './logging.js'; + +export type ManagedProcessOptions = { + name: string; + command: string; + args: string[]; + cwd: string; + env: NodeJS.ProcessEnv; + persistent?: boolean; + maxRestarts?: number; + restartWindowMs?: number; + restartBackoffMaxMs?: number; + gracefulStop?: { + command: string; + args: string[]; + timeoutMs?: number; + }; +}; + +export class ProcessSupervisor { + private readonly processes = new Map(); + + constructor( + private readonly log: DesktopLog, + private readonly pidFilePath: string, + private readonly onRestartLimitReached?: (name: string) => void, + ) {} + + start(options: ManagedProcessOptions): void { + if (this.processes.has(options.name)) { + return; + } + + const process = new ManagedProcess(options, this.log, this.onRestartLimitReached, () => this.recordPids()); + this.processes.set(options.name, process); + process.start(); + } + + async stopAll(): Promise { + const shutdownOrder = ['vito-queue', 'vito-scheduler', 'vito-websocket', 'vito-http']; + const ordered = shutdownOrder + .map((name) => this.processes.get(name)) + .filter((process): process is ManagedProcess => process !== undefined); + const remaining = [...this.processes.entries()] + .filter(([name]) => !shutdownOrder.includes(name)) + .map(([, process]) => process); + + for (const process of [...ordered, ...remaining]) { + await process.stop(); + } + this.processes.clear(); + rmSync(this.pidFilePath, { force: true }); + } + + private recordPids(): void { + const entries = [...this.processes.values()] + .map((process) => process.describe()) + .filter((entry): entry is StaleProcessEntry => entry !== null); + + try { + writeFileSync(this.pidFilePath, JSON.stringify(entries), 'utf8'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.log.error(`Unable to record supervised process pids: ${message}`); + } + } +} + +type StaleProcessEntry = { + pid: number; + name: string; + command: string; +}; + +export function cleanupStaleProcesses(pidFilePath: string, log: DesktopLog): void { + if (!existsSync(pidFilePath)) { + return; + } + + try { + const entries = JSON.parse(readFileSync(pidFilePath, 'utf8')) as StaleProcessEntry[]; + + for (const entry of entries) { + if (typeof entry?.pid !== 'number' || typeof entry?.command !== 'string') { + continue; + } + + for (const pid of findStalePids(entry)) { + log.info(`Killing stale ${entry.name} process ${pid} left over from a previous session`); + + try { + process.kill(pid, 'SIGKILL'); + } catch { + continue; + } + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log.error(`Unable to clean up stale processes: ${message}`); + } + + rmSync(pidFilePath, { force: true }); +} + +function findStalePids(entry: StaleProcessEntry): number[] { + try { + if (process.platform === 'win32') { + const output = execFileSync('tasklist', ['/FI', `PID eq ${entry.pid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf8' }); + + return output.toLowerCase().includes(basename(entry.command.split(' ')[0]).toLowerCase()) ? [entry.pid] : []; + } + + const output = execFileSync('ps', ['-axo', 'pid=,pgid=,command='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }); + const pids: number[] = []; + + for (const line of output.split('\n')) { + const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/); + if (!match) { + continue; + } + + const pid = Number(match[1]); + const pgid = Number(match[2]); + + if ((pid === entry.pid || pgid === entry.pid) && match[3].includes(entry.command)) { + pids.push(pid); + } + } + + return pids; + } catch { + return []; + } +} + +function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void { + if (process.platform !== 'win32' && typeof child.pid === 'number' && killProcessGroup(child.pid, signal)) { + return; + } + + child.kill(signal); +} + +function killProcessGroup(pid: number, signal: NodeJS.Signals): boolean { + try { + process.kill(-pid, signal); + + return true; + } catch { + return false; + } +} + +class ManagedProcess { + private child: ChildProcessWithoutNullStreams | null = null; + private stopping = false; + private restarts: number[] = []; + private consecutiveRestarts = 0; + private startedAt = 0; + private restartTimer: NodeJS.Timeout | null = null; + + constructor( + private readonly options: ManagedProcessOptions, + private readonly log: DesktopLog, + private readonly onRestartLimitReached?: (name: string) => void, + private readonly onChange?: () => void, + ) {} + + describe(): { pid: number; name: string; command: string } | null { + if (!this.child || typeof this.child.pid !== 'number') { + return null; + } + + return { + pid: this.child.pid, + name: this.options.name, + command: [this.options.command, ...this.options.args].join(' '), + }; + } + + start(): void { + this.stopping = false; + this.startedAt = Date.now(); + this.log.info(`Starting ${this.options.name}: ${this.options.command} ${this.options.args.join(' ')}`); + + this.child = spawn(this.options.command, this.options.args, { + cwd: this.options.cwd, + env: this.options.env, + stdio: 'pipe', + windowsHide: true, + detached: process.platform !== 'win32', + }); + + this.onChange?.(); + + this.child.stdout.on('data', (chunk: Buffer) => this.log.line(this.options.name, chunk.toString())); + this.child.stderr.on('data', (chunk: Buffer) => this.log.line(`${this.options.name}:stderr`, chunk.toString())); + + this.child.on('error', (error) => { + this.log.error(`${this.options.name} failed to start: ${error.message}`); + }); + + this.child.on('exit', (code, signal) => { + this.log.info(`${this.options.name} exited with code=${code ?? 'null'} signal=${signal ?? 'null'}`); + this.child = null; + this.onChange?.(); + + if (Date.now() - this.startedAt >= 60000) { + this.consecutiveRestarts = 0; + } + + if (!this.stopping && this.options.persistent !== false && this.canRestart()) { + const delay = Math.min(1000 * 2 ** this.consecutiveRestarts, this.options.restartBackoffMaxMs ?? 30000); + this.consecutiveRestarts += 1; + this.log.info(`Restarting ${this.options.name} in ${delay}ms`); + this.restartTimer = setTimeout(() => { + this.restartTimer = null; + this.start(); + }, delay); + } + }); + } + + async stop(timeoutMs = 8000): Promise { + this.stopping = true; + + if (this.restartTimer) { + clearTimeout(this.restartTimer); + this.restartTimer = null; + } + + if (!this.child || this.child.exitCode !== null || this.child.signalCode !== null) { + return; + } + + const child = this.child; + + if (this.options.gracefulStop) { + try { + await runCommand(this.options.gracefulStop.command, this.options.gracefulStop.args, { + cwd: this.options.cwd, + env: this.options.env, + log: this.log, + name: `${this.options.name}:graceful-stop`, + timeoutMs: this.options.gracefulStop.timeoutMs ?? 30000, + }); + await this.waitForExit(child, timeoutMs); + return; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.log.error(`${this.options.name} graceful stop failed: ${message}`); + } + } + + await this.terminate(child, timeoutMs); + } + + private waitForExit(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + if (child.exitCode !== null || child.signalCode !== null) { + resolve(); + return; + } + + const timeout = setTimeout(() => { + reject(new Error(`${this.options.name} did not exit gracefully`)); + }, timeoutMs); + + child.once('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } + + private async terminate(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise { + await new Promise((resolve) => { + const timeout = setTimeout(() => { + if (child.exitCode === null && child.signalCode === null) { + killProcessTree(child, 'SIGKILL'); + } + resolve(); + }, timeoutMs); + + child.once('exit', () => { + clearTimeout(timeout); + resolve(); + }); + + killProcessTree(child, 'SIGTERM'); + }); + } + + private canRestart(): boolean { + const restartWindowMs = this.options.restartWindowMs ?? 60000; + const maxRestarts = this.options.maxRestarts ?? Number.POSITIVE_INFINITY; + const now = Date.now(); + + this.restarts = this.restarts.filter((timestamp) => now - timestamp < restartWindowMs); + + if (this.restarts.length >= maxRestarts) { + this.log.error(`${this.options.name} restart limit reached`); + this.onRestartLimitReached?.(this.options.name); + return false; + } + + this.restarts.push(now); + return true; + } +} + +export function runCommand( + command: string, + args: string[], + options: { cwd: string; env: NodeJS.ProcessEnv; log: DesktopLog; name: string; timeoutMs?: number }, +): Promise { + options.log.info(`Running ${options.name}: ${command} ${args.join(' ')}`); + + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env, + stdio: 'pipe', + windowsHide: true, + }); + + const timeout = options.timeoutMs + ? setTimeout(() => { + child.kill('SIGKILL'); + reject(new Error(`${options.name} timed out`)); + }, options.timeoutMs) + : null; + + child.stdout.on('data', (chunk: Buffer) => options.log.line(options.name, chunk.toString())); + child.stderr.on('data', (chunk: Buffer) => options.log.line(`${options.name}:stderr`, chunk.toString())); + child.on('error', reject); + child.on('exit', (code) => { + if (timeout) { + clearTimeout(timeout); + } + + if (code === 0) { + resolve(); + } else { + reject(new Error(`${options.name} exited with code ${code}`)); + } + }); + }); +} diff --git a/desktop/src/runtime-paths.ts b/desktop/src/runtime-paths.ts new file mode 100644 index 000000000..c5d0e4973 --- /dev/null +++ b/desktop/src/runtime-paths.ts @@ -0,0 +1,177 @@ +import { app } from 'electron'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { appKey, readEnvFile, secret, writeEnvFile } from './env-file.js'; +import { getAvailablePort } from './ports.js'; + +export type RuntimeContext = { + appRoot: string; + dataPath: string; + envPath: string; + envFile: string; + storagePath: string; + runtimePath: string; + bootstrapCachePath: string; + logPath: string; + httpPort: number; + websocketPort: number; + appUrl: string; + phpBinary: string; + phpArgs: string[]; + env: NodeJS.ProcessEnv; + redactions: string[]; +}; + +export async function createRuntimeContext(): Promise { + const dataPath = app.getPath('userData'); + const runtimePath = join(dataPath, 'runtime'); + const envPath = runtimePath; + const envFile = join(envPath, '.env'); + const storagePath = join(dataPath, 'storage'); + const bootstrapCachePath = join(runtimePath, 'cache'); + const logPath = join(storagePath, 'logs', 'desktop-processes.log'); + const httpPort = await getAvailablePort(); + const websocketPort = await getAvailablePort(); + const appUrl = `http://127.0.0.1:${httpPort}`; + const appRoot = resolveAppRoot(); + const phpBinary = resolvePhpBinary(); + const phpArgs = resolvePhpArgs(phpBinary); + + ensureRuntimeDirectories(storagePath, runtimePath, bootstrapCachePath); + ensureFile(join(storagePath, 'database.sqlite')); + + const existing = readEnvFile(envFile); + const envValues = { + ...existing, + APP_NAME: existing.APP_NAME || 'Vito', + APP_ENV: 'production', + APP_KEY: existing.APP_KEY || appKey(), + APP_DEBUG: 'false', + APP_URL: appUrl, + VITO_DESKTOP: 'true', + VITO_DATA_PATH: dataPath, + VITO_ENV_PATH: envPath, + VITO_STORAGE_PATH: storagePath, + APP_SERVICES_CACHE: join(bootstrapCachePath, 'services.php'), + APP_PACKAGES_CACHE: join(bootstrapCachePath, 'packages.php'), + APP_CONFIG_CACHE: join(bootstrapCachePath, 'config.php'), + APP_ROUTES_CACHE: join(bootstrapCachePath, 'routes.php'), + APP_EVENTS_CACHE: join(bootstrapCachePath, 'events.php'), + VIEW_COMPILED_PATH: join(storagePath, 'framework', 'views'), + DB_CONNECTION: 'sqlite', + DB_DATABASE: 'database.sqlite', + QUEUE_CONNECTION: 'database', + DB_QUEUE: 'default', + QUEUE_FAILED_DRIVER: 'database-uuids', + CACHE_DRIVER: 'file', + SESSION_DRIVER: 'file', + FILESYSTEM_DISK: 'local', + INERTIA_ENSURE_PAGES_EXIST: 'false', + MAIL_MAILER: existing.MAIL_MAILER || 'log', + WS_HOST: '127.0.0.1', + WS_PORT: String(websocketPort), + WS_ALLOWED_ORIGINS: appUrl, + WS_BROADCAST_SECRET: existing.WS_BROADCAST_SECRET || secret(), + }; + + writeEnvFile(envFile, envValues); + + const env: NodeJS.ProcessEnv = { + ...process.env, + ...envValues, + VITO_ENV_PATH: envPath, + VITO_STORAGE_PATH: storagePath, + }; + + const caBundle = join(dirname(phpBinary), 'cacert.pem'); + if (existsSync(caBundle)) { + env.SSL_CERT_FILE = caBundle; + env.CURL_CA_BUNDLE = caBundle; + } + + return { + appRoot, + dataPath, + envPath, + envFile, + storagePath, + runtimePath, + bootstrapCachePath, + logPath, + httpPort, + websocketPort, + appUrl, + phpBinary, + phpArgs, + env, + redactions: [envValues.APP_KEY, envValues.WS_BROADCAST_SECRET], + }; +} + +function ensureRuntimeDirectories(storagePath: string, runtimePath: string, bootstrapCachePath: string): void { + const directories = [ + runtimePath, + bootstrapCachePath, + join(storagePath, 'app'), + join(storagePath, 'app', 'public'), + join(storagePath, 'framework'), + join(storagePath, 'framework', 'cache'), + join(storagePath, 'framework', 'cache', 'data'), + join(storagePath, 'framework', 'sessions'), + join(storagePath, 'framework', 'views'), + join(storagePath, 'logs'), + join(storagePath, 'plugins'), + ]; + + for (const directory of directories) { + mkdirSync(directory, { recursive: true, mode: 0o700 }); + } +} + +function ensureFile(path: string): void { + if (!existsSync(path)) { + writeFileSync(path, '', { mode: 0o600 }); + } +} + +function resolveAppRoot(): string { + if (app.isPackaged) { + return join(process.resourcesPath, 'app'); + } + + return resolve(app.getAppPath(), '..'); +} + +function resolvePhpBinary(): string { + if (process.env.PHP_BINARY) { + return process.env.PHP_BINARY; + } + + if (!app.isPackaged) { + return 'php'; + } + + const exe = process.platform === 'win32' ? 'php.exe' : 'php'; + return join(process.resourcesPath, 'bin', process.platform, process.arch, exe); +} + +function resolvePhpArgs(phpBinary: string): string[] { + const args: string[] = []; + const binDir = dirname(phpBinary); + + if (app.isPackaged) { + args.push('-d', 'memory_limit=512M'); + } + + const caBundle = join(binDir, 'cacert.pem'); + if (existsSync(caBundle)) { + args.push('-d', `curl.cainfo=${caBundle}`, '-d', `openssl.cafile=${caBundle}`); + } + + const extensionDir = join(binDir, 'ext'); + if (process.platform === 'win32' && existsSync(extensionDir)) { + args.push('-d', `extension_dir=${extensionDir}`); + } + + return args; +} diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 000000000..ff8154067 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node", "electron"] + }, + "include": ["src/**/*.ts"] +} diff --git a/docs/desktop-app-plan.md b/docs/desktop-app-plan.md new file mode 100644 index 000000000..2b2b49b84 --- /dev/null +++ b/docs/desktop-app-plan.md @@ -0,0 +1,793 @@ +# Vito Desktop App Conversion Plan + +Status: draft +Date: 2026-07-05 + +## Summary + +Vito should become a desktop app by keeping the Laravel application mostly intact and adding a desktop runtime around it. The stable production path is a desktop shell that starts a local Laravel backend, supervises its background processes, stores all mutable data outside the installed app bundle, and opens the existing Inertia/React UI in a native desktop window. + +Recommended production path: + +1. Build a custom Electron shell. +2. Bundle a PHP runtime, preferably FrankenPHP if the required PHP 8.4 extensions are available. +3. Run Laravel locally on `127.0.0.1`. +4. Move desktop mode away from Redis/Horizon by default and use SQLite-backed queues plus file or database cache/session. +5. Run the websocket server, queue workers, and scheduler as supervised child processes. +6. Add per-platform installers, code signing, auto-update, and smoke tests. + +NativePHP should still be spiked first because it may give a faster Laravel-native path. It should be accepted only if it handles Vito's PHP 8.4 extension needs, queue topology, scheduler, websocket child process, build signing, and updates without fragile custom work. + +## Goals + +- Ship a normal desktop installer for macOS, Windows, and Linux. +- Do not require users to install PHP, Composer, Node, Redis, Docker, or a web server. +- Preserve the existing Laravel/Inertia app as the primary product surface. +- Preserve long-running server-management workflows: SSH jobs, backups, metrics, SSL checks, websocket events, and scheduled tasks. +- Keep user data stable across app upgrades. +- Make process startup, shutdown, crash recovery, migrations, and updates deterministic. +- Keep Docker-based deployment untouched for the existing server product. + +## Non-goals + +- Rewriting the app as a native Swift, Kotlin, .NET, or Rust UI. +- Building a fully offline server-management tool. Vito still needs network access to managed servers and external APIs. +- Running the desktop backend as a system service after the app quits. +- Shipping Docker Desktop as a required dependency. +- Removing the hosted/web version architecture. + +## Current Repository Constraints + +- The app requires PHP `^8.4` plus `ext-ftp`, `ext-intl`, and `ext-zip` in `composer.json`. +- The frontend is already Inertia/React/Vite, so the UI can run inside Electron, NativePHP, or Tauri with minimal UI rewrite. +- The app currently depends on Redis for queues, cache, and session by default: + - `config/queue.php` defaults to a Redis-backed `default` connection. + - `config/cache.php` defaults to Redis. + - `config/session.php` defaults to Redis. +- Horizon is required by Composer and used as the queue worker in Docker. +- The app already has a SQLite default database connection. +- A `jobs` migration already exists, so database queues are feasible. +- The app schedules many operational commands in `app/Console/Kernel.php`, including every-minute tasks. +- The app has a custom websocket command, `php artisan ws:serve`, defaulting to `127.0.0.1:8085`. +- The Docker runtime already proves the needed process set: web server, Redis, migrations, queue worker, scheduler/cron, and websocket. + +## Decision: Production Architecture + +Use a custom Electron shell with a bundled Laravel backend. + +### Why Electron + +Electron is heavier than Tauri, but it is the lowest-risk custom desktop shell for this app because: + +- It has mature packaging, signing, auto-update, and installer ecosystems. +- It can supervise multiple child processes from a predictable Node main process. +- It lets the Laravel app continue serving normal HTTP routes to Chromium. +- NativePHP itself uses Electron under the hood for desktop, so the fallback is aligned with the Laravel-first path. + +### Why Not Docker Desktop + +Docker is useful for development and server deployment, but not for a stable consumer-style desktop app: + +- It requires a large external dependency. +- Windows/macOS installation and permissions are support-heavy. +- Startup can be slow and resource-hungry. +- Updates become a mix of app updates, image updates, volumes, ports, and Docker state. +- It makes code signing and native app lifecycle harder. + +### Why Reduce Redis/Horizon in Desktop Mode + +Redis and Horizon are excellent for the server product, but they add a local service dependency to desktop packaging. For a single-user desktop app, SQLite-backed jobs are simpler and more stable: + +- Fewer bundled binaries. +- Fewer ports and processes. +- Easier crash recovery. +- Easier backup and migration. +- Good enough queue throughput for local use. + +Desktop mode should use: + +```env +DB_CONNECTION=sqlite +QUEUE_CONNECTION=database +CACHE_DRIVER=file +SESSION_DRIVER=file +``` + +If database queues cannot handle a specific high-concurrency workload, add a desktop-only Redis sidecar later. Do not make Redis the first production desktop design. + +## Architecture + +### Process Model + +The Electron main process is the desktop supervisor. It owns the lifecycle of all backend processes. + +Required processes: + +1. `vito-http` + - Starts Laravel on a loopback HTTP port. + - Preferred command: bundled FrankenPHP serving `public/`. + - Fallback command: bundled PHP with a production-grade local server strategy. + +2. `vito-queue` + - Runs Laravel queue workers. + - Command shape: + + ```sh + php artisan queue:work database --queue=default,ssh,ssh-certbot --sleep=3 --timeout=1200 --tries=1 + ``` + + - A separate long-timeout worker may be needed for backup jobs: + + ```sh + php artisan queue:work database --queue=ssh,ssh-certbot --sleep=3 --timeout=3660 --tries=1 + ``` + +3. `vito-scheduler` + - Runs scheduled tasks while the app is open. + - Command: + + ```sh + php artisan schedule:work + ``` + +4. `vito-websocket` + - Runs the existing websocket server. + - Command: + + ```sh + php artisan ws:serve --host=127.0.0.1 --port= + ``` + +Optional process: + +5. `vito-redis` + - Only if the database queue path fails acceptance testing. + - Must be a bundled sidecar, never a user prerequisite. + +### Startup Flow + +1. Acquire a single-instance app lock. +2. Resolve the app data directory: + - macOS: `~/Library/Application Support/Vito` + - Windows: `%APPDATA%/Vito` + - Linux: `~/.config/Vito` +3. Create app data subdirectories: + - `storage/` + - `storage/app/` + - `storage/framework/` + - `storage/logs/` + - `storage/plugins/` + - `runtime/` +4. Generate or load `.env.desktop`. +5. Allocate local ports for HTTP and websocket. +6. Write runtime env values: + - `APP_URL=http://127.0.0.1:` + - `WS_HOST=127.0.0.1` + - `WS_PORT=` + - `WS_ALLOWED_ORIGINS=http://127.0.0.1:` +7. Ensure `APP_KEY` exists. +8. Ensure SSH keypair exists. +9. Ensure SQLite database exists. +10. Run `php artisan migrate --force`. +11. Run safe optimization for production: + - `php artisan optimize:clear` + - `php artisan optimize` +12. Start `vito-websocket`. +13. Start `vito-queue`. +14. Start `vito-scheduler`. +15. Start `vito-http`. +16. Poll `/api/health` until ready. +17. Open the Electron window to `APP_URL`. + +### Shutdown Flow + +1. Stop accepting new window actions. +2. Ask Laravel workers to quit gracefully where possible. +3. Stop scheduler. +4. Stop websocket server. +5. Stop HTTP server. +6. Kill remaining child processes after a timeout. +7. Release the single-instance lock. + +### Crash Recovery + +Each process must have a restart policy: + +- `vito-http`: restart up to 3 times in 60 seconds, then show a fatal backend error screen. +- `vito-websocket`: restart indefinitely with backoff, show a degraded realtime status if down. +- `vito-queue`: restart indefinitely with backoff, surface worker status in diagnostics. +- `vito-scheduler`: restart indefinitely with backoff, surface scheduler status in diagnostics. + +All child process stdout/stderr must be written to: + +```text +/storage/logs/desktop-processes.log +``` + +## Laravel Changes + +### Desktop Runtime Detection + +Add a small desktop runtime helper instead of scattering `env()` checks. + +Suggested file: + +```text +app/Support/DesktopRuntime.php +``` + +Responsibilities: + +- `enabled(): bool` +- `storagePath(): ?string` +- `httpPort(): ?int` +- `websocketPort(): ?int` +- `dataPath(string $path = ''): string` + +Environment flag: + +```env +VITO_DESKTOP=true +``` + +### Mutable Storage Path + +The installed app bundle must be treated as read-only. Desktop mode must move Laravel `storage_path()` to the per-user app data directory. + +Update `bootstrap/app.php` after the application is created: + +```php +if (($storagePath = $_ENV['VITO_STORAGE_PATH'] ?? null) !== null) { + $app->useStoragePath($storagePath); +} +``` + +The Electron shell should set: + +```env +VITO_STORAGE_PATH=/storage +``` + +### Desktop Env File + +Add: + +```text +.env.desktop.example +``` + +Baseline: + +```env +APP_NAME=Vito +APP_ENV=production +APP_DEBUG=false +APP_URL=http://127.0.0.1 +APP_KEY= + +VITO_DESKTOP=true +VITO_DATA_PATH= +VITO_ENV_PATH= +VITO_STORAGE_PATH= + +APP_SERVICES_CACHE= +APP_PACKAGES_CACHE= +APP_CONFIG_CACHE= +APP_ROUTES_CACHE= +APP_EVENTS_CACHE= +VIEW_COMPILED_PATH= + +DB_CONNECTION=sqlite +DB_DATABASE=database.sqlite + +QUEUE_CONNECTION=database +DB_QUEUE=default +QUEUE_FAILED_DRIVER=database-uuids + +CACHE_DRIVER=file +SESSION_DRIVER=file +FILESYSTEM_DISK=local + +WS_HOST=127.0.0.1 +WS_PORT=8085 +WS_ALLOWED_ORIGINS= +WS_BROADCAST_SECRET= + +MAIL_MAILER=log +``` + +### Queue Configuration + +Add an explicit database queue connection to `config/queue.php`: + +```php +'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'jobs', + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => max(300, (int) env('BACKUP_RUN_TIMEOUT', 3600)) + 60, + 'after_commit' => false, +], +``` + +Desktop workers must consume all relevant queue names: + +```text +default,ssh,ssh-certbot +``` + +### Cache And Session + +Use file cache and file sessions for the first desktop build. This avoids adding a cache table migration. + +If file locking becomes unreliable on Windows under load, switch desktop cache/session to database and add the standard Laravel cache table migration. + +### Horizon + +Do not run Horizon in desktop mode. + +Options: + +1. Keep `laravel/horizon` installed for server deployments. +2. Disable Horizon routes/features in desktop mode if needed. +3. Add a lightweight desktop diagnostics page later that reads queue state from the `jobs` and `failed_jobs` tables. + +### Websocket Configuration + +The websocket port must become dynamic in desktop mode. Avoid hardcoding `8085` because it may already be in use. + +Electron should allocate the port and pass it through env. Laravel should expose the websocket URL in the frontend bootstrap data. + +### Desktop Authentication + +Desktop mode should keep Laravel's session guard, but replace the normal web auth pages with a desktop-local user picker. + +Implemented behavior: + +1. Desktop-only routes live under `/desktop`. +2. Unauthenticated desktop requests redirect to `desktop.login`. +3. The desktop login screen lists existing local users. +4. Before the app has been locked, choosing a user logs in without showing the normal email/password page. +5. Locking the desktop app logs the current user out, marks the session as locked, and redirects to the desktop login screen. +6. Unlocking after a lock requires the selected user's password. +7. Fortify login, password reset, password confirmation, and two-factor challenge views redirect back to the desktop picker in desktop mode. +8. If no users exist, the desktop login screen becomes a first-run setup form that creates the first local admin user, creates its default project, and logs it in. + +Desktop must not ship with a fixed default password. The setup route must stay desktop-only and must reject requests after any user exists. During development and smoke testing, `php artisan user:create` remains a valid way to seed users manually. + +## Desktop Shell Structure + +Suggested repo layout: + +```text +desktop/ + package.json + tsconfig.json + electron-builder.yml + src/ + main.ts + preload.ts + process-supervisor.ts + runtime-paths.ts + first-run.ts + health-check.ts + logging.ts + resources/ + bin/ + mac-arm64/ + mac-x64/ + win-x64/ + linux-x64/ + icons/ +``` + +Generated build artifact layout: + +```text +desktop/dist/ + app/ + artisan + bootstrap/ + config/ + database/ + public/ + resources/ + routes or route-attribute cache output + storage-template/ + vendor/ +``` + +Do not copy development-only files into the desktop app: + +- `node_modules/` +- `tests/` +- `.git/` +- local `.env` +- Docker files unless needed for source compliance bundles + +## Build Pipeline + +### Local Build Script + +Add a top-level script later: + +```sh +composer desktop:prepare +npm run desktop:build +``` + +The preparation step should: + +1. Install Composer dependencies with `--no-dev --optimize-autoloader`. +2. Run `npm ci`. +3. Run `npm run build`. +4. Create a clean Laravel distribution directory. +5. Copy required runtime files. +6. Validate required PHP extensions against the bundled PHP binary. +7. Package Electron. + +### CI Matrix + +Build on each target OS, not by cross-compiling everything from one machine: + +- macOS arm64 +- macOS x64 if supported +- Windows x64 +- Linux x64 + +Each CI job must run: + +```sh +composer install +npm ci +npm run build +php artisan test +desktop build +desktop smoke test +``` + +### Code Signing And Notarization + +macOS: + +- Sign the app bundle. +- Notarize release artifacts. +- Verify first launch on a clean machine. + +Windows: + +- Sign installer and executable. +- Prefer MSIX or NSIS depending on updater choice. + +Linux: + +- AppImage first for portability. +- Add `.deb` later if there is demand. + +### Auto Updates + +Preferred Electron path: + +- Use `electron-builder` plus `electron-updater`. +- Host releases on GitHub Releases, S3, or a dedicated update endpoint. +- Stable and beta channels should be separate. + +Update safety requirements: + +- Never overwrite app data. +- Run migrations only after the new backend starts. +- Backup the SQLite database before migrations. +- Roll back the app binary if health checks fail after update. +- Keep release notes visible before applying updates. + +## NativePHP Spike + +Run this before committing fully to custom Electron. + +### Spike Steps + +1. Create a throwaway branch. +2. Install NativePHP Desktop. +3. Add `.env.desktop.example`. +4. Configure database queues, file cache, and file sessions. +5. Configure NativePHP queue workers for: + - `default` + - `ssh` + - `ssh-certbot` +6. Start `ws:serve` as a persistent child process. +7. Verify scheduler behavior. +8. Build a macOS app locally. +9. Confirm the app launches without installed PHP, Composer, Node, Redis, or Docker. +10. Confirm required PHP extensions: + - ftp + - intl + - zip + - sqlite/pdo_sqlite + - openssl + - curl +11. Run core workflows: + - first-run admin creation + - server creation flow up to SSH validation + - queue job execution + - websocket terminal/events + - scheduled metrics/check tasks + - app restart + - migration after version bump + +### Accept NativePHP If + +- Packaged app starts reliably on macOS and at least one other target OS. +- Required PHP extensions are available or custom binaries are practical. +- Queue workers can process `default`, `ssh`, and `ssh-certbot` without Horizon. +- Scheduler runs while the app is open. +- `ws:serve` can be supervised and restarted. +- User storage lives outside the signed app bundle. +- Auto-update and signing path is acceptable. + +### Reject NativePHP If + +- PHP 8.4 or required extensions require unsupported custom binaries. +- Background workers or websocket process supervision is fragile. +- Build customization requires forking too much NativePHP internals. +- Update/signing behavior cannot be tested cleanly. + +If rejected, continue with the custom Electron path using lessons from the spike. + +## Tauri Option + +Tauri remains a viable second fallback if app size becomes a hard requirement. It should not be the first implementation because it requires more custom process supervision and less Laravel-specific tooling. + +Use Tauri only if: + +- Electron bundle size is unacceptable. +- The team is comfortable maintaining Rust/Tauri process orchestration. +- The sidecar binary pipeline for PHP/FrankenPHP is proven on all target OSes. + +## Security Requirements + +- Bind all local services to `127.0.0.1`, never `0.0.0.0`. +- Use dynamic ports where possible. +- Enforce single-instance mode. +- Set Electron `nodeIntegration: false`. +- Set Electron `contextIsolation: true`. +- Expose only minimal APIs through `preload.ts`. +- Restrict websocket allowed origins to the current local app URL. +- Generate a desktop-local broadcast secret on first run. +- Do not ship default credentials. +- Do not store secrets in the installed app bundle. +- Keep private SSH keys in the app data directory with restrictive permissions. +- Redact secrets in desktop process logs. + +## Diagnostics + +Add a desktop diagnostics screen or command before public release. + +It should show: + +- App version. +- Backend version. +- App data path. +- HTTP port. +- Websocket port. +- Database path. +- Process status for HTTP, queue, scheduler, websocket. +- Last 200 lines of desktop process logs. +- Last 200 lines of Laravel logs. +- Queue depth by queue name. +- Failed job count. +- Button to export a support bundle. + +Support bundle must redact: + +- APP_KEY +- WS_BROADCAST_SECRET +- API tokens +- SSH private keys +- provider credentials + +## Testing Plan + +### Unit And Feature Tests + +Keep existing tests. Add desktop-specific feature tests for: + +- desktop storage path override +- desktop env generation +- database queue config +- first-run user creation guard +- websocket URL generation +- desktop user picker login +- desktop lock and password unlock + +### Process Integration Tests + +Run from the desktop package: + +1. Start app. +2. Wait for `/api/health`. +3. Assert SQLite database exists in app data. +4. Assert migrations are applied. +5. Assert queue worker processes a test job. +6. Assert scheduler process is running. +7. Assert websocket port accepts connection. +8. Restart app and verify no duplicate processes. +9. Quit app and verify child processes are gone. + +### Packaging Smoke Tests + +For each target OS: + +1. Install fresh app. +2. Launch app. +3. Complete first-run admin setup. +4. Reload app. +5. Quit app. +6. Relaunch app. +7. Verify login persists. +8. Verify logs are created in app data. +9. Verify no writes happen inside the signed app bundle. + +### Update Tests + +1. Install version N. +2. Create user and sample data. +3. Install/update to version N+1. +4. Verify app data remains. +5. Verify migrations run once. +6. Verify rollback behavior on forced failed health check. + +## Implementation Phases + +### Phase 0: NativePHP Decision Spike + +Deliverables: + +- Spike branch. +- Written decision note. +- List of extension/build blockers. +- Pass/fail against acceptance criteria. + +Exit: + +- Choose NativePHP or custom Electron. + +### Phase 1: Desktop Runtime Foundation + +Deliverables: + +- `.env.desktop.example` +- `VITO_DESKTOP` runtime detection. +- `VITO_STORAGE_PATH` support in `bootstrap/app.php`. +- Database queue connection in `config/queue.php`. +- File cache/session desktop profile. +- First-run admin creation plan or route. + +Exit: + +- Laravel can run locally in desktop mode without Redis. + +### Phase 2: Electron Shell Prototype + +Deliverables: + +- `desktop/` package. +- Electron main window. +- Runtime path resolver. +- Process supervisor. +- Health check polling. +- App data directory creation. +- Dynamic port allocation. + +Exit: + +- `npm run desktop:dev` launches Vito through Electron. + +### Phase 3: Background Process Reliability + +Deliverables: + +- Supervised HTTP, queue, scheduler, websocket processes. +- Restart/backoff policies. +- Unified process logging. +- Single-instance lock. +- Graceful shutdown. + +Exit: + +- App restart/quit leaves no orphaned child processes. + +### Phase 4: Desktop UX And Diagnostics + +Deliverables: + +- First-run admin setup. +- Backend starting screen. +- Backend error screen. +- Diagnostics/export support bundle. +- Update/restart prompts. + +Exit: + +- Non-technical user can install, start, diagnose, and update the app. + +### Phase 5: Packaging And Signing + +Deliverables: + +- macOS signed/notarized build. +- Windows signed installer. +- Linux AppImage. +- CI matrix builds. +- Release artifact publishing. + +Exit: + +- A clean machine can install and run each platform build. + +### Phase 6: Auto Update + +Deliverables: + +- Stable and beta update channels. +- Update download/install flow. +- Database backup before migration. +- Failed-update health check handling. + +Exit: + +- Version N updates to N+1 without data loss. + +### Phase 7: Beta Hardening + +Deliverables: + +- 20 to 30 real workflow test runs. +- Long-running backup/SSH job tests. +- Sleep/wake tests. +- Offline/online transition tests. +- Port conflict tests. +- Antivirus/Defender check on Windows. + +Exit: + +- Known blocking bugs fixed or explicitly accepted. + +## Work Checklist + +- [x] Decide NativePHP vs custom Electron: custom Electron shell bundling a static PHP built with static-php-cli (the same runtime approach NativePHP uses). +- [x] Add desktop env profile. +- [x] Add storage path override. +- [x] Add database queue connection. +- [x] Confirm jobs table migration covers all queue needs. +- [x] Remove Redis requirement from desktop mode. +- [ ] Disable Horizon runtime in desktop mode. +- [x] Make websocket port dynamic. +- [x] Add desktop first-run admin setup. +- [x] Add Electron shell. +- [x] Add process supervisor. +- [x] Add app data path management. +- [x] Add health check based launch. +- [x] Add graceful shutdown. +- [ ] Add diagnostics. +- [x] Add desktop smoke tests (scripts/desktop/smoke-test.sh, run in CI). +- [x] Add packaging (electron-builder + scripts/desktop + desktop-release workflow). +- [x] Add code signing (optional GitHub secrets, unsigned builds without them). +- [ ] Add auto-update. +- [ ] Run beta hardening matrix. + +## Open Questions + +- Which platforms are required for v1: macOS only, or macOS plus Windows/Linux? +- Does the desktop app need to run jobs while the window is closed but the app is still in the tray? +- Should desktop support plugins from `storage/plugins/*/*` in v1? +- Should the desktop app use the same AGPL distribution model, or dual-license the desktop release? +- Should updates be hosted on GitHub Releases, S3, or the Vito website? +- Is app-store distribution required, or are direct downloads enough? +- Should the first desktop beta keep Horizon available behind a debug flag for comparison? + +## Initial Recommendation + +Run the NativePHP spike first, but plan production around custom Electron unless the spike clears every process, extension, and update requirement cleanly. + +The custom Electron path is more work, but it gives Vito direct control over the exact things that matter for stability: process supervision, local data paths, updates, diagnostics, and platform-specific packaging. diff --git a/eslint.config.js b/eslint.config.js index e3d7b1934..6160f3642 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -37,7 +37,7 @@ export default [ }, }, { - ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'], + ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js', 'desktop/dist', 'desktop/release'], }, prettier, // Turn off all rules that might conflict with Prettier ]; diff --git a/package.json b/package.json index d3297e0b4..49f862729 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,11 @@ "type": "module", "scripts": { "build": "vite build", + "build:desktop": "vite build --mode desktop", "build:ssr": "vite build && vite build --ssr", + "desktop:build": "npm --prefix desktop run build", + "desktop:dev": "npm --prefix desktop run dev", + "desktop:package": "npm --prefix desktop run package", "dev": "vite", "format": "prettier --write resources/", "format:check": "prettier --check resources/", diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx index 8ffc55b52..c530d103c 100644 --- a/resources/js/components/user-menu-content.tsx +++ b/resources/js/components/user-menu-content.tsx @@ -1,9 +1,10 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'; import { UserInfo } from '@/components/user-info'; import { useMobileNavigation } from '@/hooks/use-mobile-navigation'; +import { type SharedData } from '@/types'; import { type User } from '@/types/user'; -import { Link, router } from '@inertiajs/react'; -import { LogOut, Settings } from 'lucide-react'; +import { Link, router, usePage } from '@inertiajs/react'; +import { LockKeyhole, LogOut, Settings } from 'lucide-react'; import AppearanceToggleTab from '@/components/appearance-tabs'; import { useBootstrapStore } from '@/stores/bootstrap-store'; @@ -13,8 +14,10 @@ interface UserMenuContentProps { export function UserMenuContent({ user }: UserMenuContentProps) { const cleanup = useMobileNavigation(); + const { desktop } = usePage().props; + const isDesktop = desktop?.enabled === true; - const handleLogout = () => { + const handleSessionExit = () => { cleanup(); router.flushAll(); useBootstrapStore.getState().clear(); @@ -40,9 +43,9 @@ export function UserMenuContent({ user }: UserMenuContentProps) { - - - Log out + + {isDesktop ? : } + {isDesktop ? 'Lock app' : 'Log out'} diff --git a/resources/js/pages/auth/desktop-login.tsx b/resources/js/pages/auth/desktop-login.tsx new file mode 100644 index 000000000..147ff3bf7 --- /dev/null +++ b/resources/js/pages/auth/desktop-login.tsx @@ -0,0 +1,231 @@ +import InputError from '@/components/ui/input-error'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; +import AuthLayout from '@/layouts/auth/layout'; +import { UserInfo } from '@/components/user-info'; +import { type User } from '@/types/user'; +import { Head, router, useForm, usePage } from '@inertiajs/react'; +import { LoaderCircleIcon, LockKeyholeIcon, UserPlusIcon } from 'lucide-react'; +import { FormEvent, useMemo, useState } from 'react'; + +type DesktopLoginProps = { + users: User[]; + setup_required: boolean; + locked: boolean; + locked_user_id: number | null; +}; + +export default function DesktopLogin() { + const { users, setup_required, locked, locked_user_id } = usePage().props; + const [selectedUserId, setSelectedUserId] = useState(locked_user_id); + const [choosing, setChoosing] = useState(false); + const loginForm = useForm<{ password: string; code: string }>({ + password: '', + code: '', + }); + const setupForm = useForm<{ + name: string; + email: string; + password: string; + password_confirmation: string; + }>({ + name: '', + email: '', + password: '', + password_confirmation: '', + }); + + const selectedUser = useMemo(() => users.find((user) => user.id === selectedUserId) ?? null, [selectedUserId, users]); + const title = setup_required ? 'Set up Vito' : locked ? 'Unlock Vito' : 'Choose a user'; + const description = setup_required + ? 'Create the first local desktop administrator.' + : locked + ? 'Enter the password for the selected user.' + : 'Select a local desktop user to continue.'; + + const chooseUser = (user: User) => { + if (!locked) { + if (choosing) { + return; + } + + setChoosing(true); + router.post( + route('desktop.login.store', { user: user.id }), + {}, + { + onFinish: () => setChoosing(false), + }, + ); + return; + } + + loginForm.clearErrors(); + loginForm.reset('password', 'code'); + setSelectedUserId(user.id); + }; + + const setup = (e: FormEvent) => { + e.preventDefault(); + + setupForm.post(route('desktop.setup'), { + onFinish: () => setupForm.reset('password', 'password_confirmation'), + }); + }; + + const unlock = (e: FormEvent) => { + e.preventDefault(); + + if (!selectedUser) { + return; + } + + loginForm.post(route('desktop.login.store', { user: selectedUser.id }), { + onFinish: () => loginForm.reset('password', 'code'), + }); + }; + + return ( + + + +
+ {setup_required ? ( +
+
+ + setupForm.setData('name', e.target.value)} + placeholder="Name" + /> + +
+ +
+ + setupForm.setData('email', e.target.value)} + placeholder="Email" + /> + +
+ +
+ + setupForm.setData('password', e.target.value)} + placeholder="Password" + /> + +
+ +
+ + setupForm.setData('password_confirmation', e.target.value)} + placeholder="Confirm password" + /> + +
+ + +
+ ) : users.length === 0 ? ( +
+

No users found.

+

Create a user before continuing.

+
+ ) : ( +
+ {users.map((user) => { + const selected = selectedUserId === user.id; + + return ( + + ); + })} +
+ )} + + {locked && selectedUser && ( +
+
+ + loginForm.setData('password', e.target.value)} + placeholder="Password" + /> + +
+ + {selectedUser.two_factor_enabled && ( +
+ + loginForm.setData('code', e.target.value)} + placeholder="Authentication or recovery code" + /> + +
+ )} + + +
+ )} +
+
+ ); +} diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index b2d725096..2cc626415 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -134,6 +134,9 @@ export interface SharedData { version: string; env: string; demo: boolean; + desktop: { + enabled: boolean; + }; quote: { message: string; author: string }; auth: Auth; ziggy?: Config & { location: string }; diff --git a/scripts/desktop/build-app.sh b/scripts/desktop/build-app.sh new file mode 100644 index 000000000..bf47feaea --- /dev/null +++ b/scripts/desktop/build-app.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Assembles the Laravel application distribution consumed by the desktop +# Electron shell into desktop-build/app. +# +# Prerequisites (run from the repository root): +# composer install --no-dev --optimize-autoloader +# npm run build:desktop +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DIST="$ROOT/desktop-build/app" + +if [ ! -f "$ROOT/vendor/autoload.php" ]; then + echo "vendor/autoload.php is missing. Run: composer install --no-dev --optimize-autoloader" >&2 + exit 1 +fi + +if [ -f "$ROOT/vendor/bin/phpunit" ]; then + echo "Warning: vendor contains dev dependencies. Run composer install with --no-dev for release builds." >&2 +fi + +if [ ! -f "$ROOT/public/build-desktop/manifest.json" ]; then + echo "public/build-desktop/manifest.json is missing. Run: npm run build:desktop" >&2 + exit 1 +fi + +rm -rf "$ROOT/desktop-build" +mkdir -p "$DIST" + +INCLUDES=( + app + bootstrap + config + database + lang + public + resources/deployment-scripts + resources/views + vendor + artisan + composer.json + composer.lock +) + +tar -cf - \ + -C "$ROOT" \ + --exclude='public/build' \ + --exclude='public/hot' \ + --exclude='public/storage' \ + --exclude='bootstrap/cache/*.php' \ + --exclude='.DS_Store' \ + "${INCLUDES[@]}" | tar -xf - -C "$DIST" + +for file in \ + "$DIST/artisan" \ + "$DIST/vendor/autoload.php" \ + "$DIST/public/index.php" \ + "$DIST/public/build-desktop/manifest.json" \ + "$DIST/resources/views/ssh/services/webserver/nginx/vhost.mustache"; do + if [ ! -f "$file" ]; then + echo "Desktop app distribution is incomplete: missing $file" >&2 + exit 1 + fi +done + +echo "Desktop app distribution assembled at $DIST" diff --git a/scripts/desktop/fetch-php.sh b/scripts/desktop/fetch-php.sh new file mode 100644 index 000000000..895b72b7a --- /dev/null +++ b/scripts/desktop/fetch-php.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# +# Provisions the portable PHP runtime bundled with the desktop app into +# desktop/resources/bin///. +# +# macOS/Linux: builds a static PHP CLI binary with static-php-cli (spc) using +# the exact extension set Vito needs. CI caches the output directory, so the +# build only runs when this script changes. +# Windows: downloads the official php.net NTS build and generates a php.ini +# enabling the required extension DLLs. +# +# Usage: scripts/desktop/fetch-php.sh +set -euo pipefail + +PLATFORM="${1:?usage: fetch-php.sh }" +ARCH="${2:?usage: fetch-php.sh }" + +PHP_SERIES="8.4" +SPC_VERSION="2.8.5" +COMMON_EXTENSIONS="bcmath,ctype,curl,dom,fileinfo,filter,ftp,gmp,iconv,intl,mbstring,mbregex,opcache,openssl,pdo,pdo_sqlite,phar,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib" +UNIX_EXTENSIONS="$COMMON_EXTENSIONS,pcntl,posix" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TARGET="$ROOT/desktop/resources/bin/$PLATFORM/$ARCH" +mkdir -p "$TARGET" + +PHP_BINARY="$TARGET/php" +if [ "$PLATFORM" = "win32" ]; then + PHP_BINARY="$TARGET/php.exe" +fi + +if [ ! -f "$TARGET/cacert.pem" ]; then + curl -fsSL --retry 3 https://curl.se/ca/cacert.pem -o "$TARGET/cacert.pem" +fi + +if [ -f "$PHP_BINARY" ]; then + echo "PHP runtime already present at $PHP_BINARY" + "$PHP_BINARY" -v + exit 0 +fi + +fetch_windows() { + local zip="$TARGET/php-windows.zip" + local releases + releases="$(curl -fsSL --retry 3 -A "vito-desktop-build" https://windows.php.net/downloads/releases/releases.json)" + + local zip_name + local zip_sha256 + zip_name="$(jq -r ".\"${PHP_SERIES}\".\"nts-vs17-x64\".zip.path" <<< "$releases")" + zip_sha256="$(jq -r ".\"${PHP_SERIES}\".\"nts-vs17-x64\".zip.sha256" <<< "$releases")" + + if [ -z "$zip_name" ] || [ "$zip_name" = "null" ] || [ -z "$zip_sha256" ] || [ "$zip_sha256" = "null" ]; then + echo "Unable to resolve the PHP ${PHP_SERIES} NTS x64 build from releases.json" >&2 + exit 1 + fi + + curl -fsSL --retry 3 -A "vito-desktop-build" \ + "https://windows.php.net/downloads/releases/${zip_name}" \ + -o "$zip" + verify_sha256 "$zip" "$zip_sha256" + unzip -qo "$zip" -d "$TARGET" + rm -f "$zip" + + cat > "$TARGET/php.ini" <<'INI' +extension_dir = "ext" +extension = curl +extension = fileinfo +extension = ftp +extension = gmp +extension = intl +extension = mbstring +extension = openssl +extension = pdo_sqlite +extension = sockets +extension = sodium +extension = sqlite3 +extension = zip +zend_extension = opcache +memory_limit = 512M +INI + + local system32="/c/Windows/System32" + for dll in vcruntime140.dll vcruntime140_1.dll msvcp140.dll; do + if [ -f "$system32/$dll" ] && [ ! -f "$TARGET/$dll" ]; then + cp "$system32/$dll" "$TARGET/" + fi + done +} + +verify_sha256() { + local file="$1" + local expected="$2" + local actual + + actual="$(shasum -a 256 "$file" | awk '{print $1}')" + + if [ "$actual" != "$expected" ]; then + echo "Checksum mismatch for $file: expected $expected, got $actual" >&2 + exit 1 + fi +} + +spc_sha256() { + case "$1" in + spc-macos-aarch64) echo "acf2f25d56d0cbf8e65aa82e5054fef555f7be7c5c38046c6e0819f266d83225" ;; + spc-macos-x86_64) echo "e8b798048f62ca4960764196543b60ae703f7174aa418824cf542aeec1d2cd6a" ;; + spc-linux-x86_64) echo "523ba4279c54c7a377156c0dd3a36adf92ee64b01e9a7f5e9e2ec084b8e458e5" ;; + *) echo "" ;; + esac +} + +fetch_unix() { + local spc_platform + local spc_arch + + case "$PLATFORM" in + darwin) spc_platform="macos" ;; + linux) spc_platform="linux" ;; + *) echo "Unsupported platform: $PLATFORM" >&2; exit 1 ;; + esac + + case "$ARCH" in + arm64) spc_arch="aarch64" ;; + x64) spc_arch="x86_64" ;; + *) echo "Unsupported arch: $ARCH" >&2; exit 1 ;; + esac + + local workdir + workdir="$(mktemp -d)" + + curl -fsSL --retry 3 \ + "https://github.com/crazywhalecc/static-php-cli/releases/download/${SPC_VERSION}/spc-${spc_platform}-${spc_arch}.tar.gz" \ + -o "$workdir/spc.tar.gz" + verify_sha256 "$workdir/spc.tar.gz" "$(spc_sha256 "spc-${spc_platform}-${spc_arch}")" + tar -xzf "$workdir/spc.tar.gz" -C "$workdir" + chmod +x "$workdir/spc" + + ( + cd "$workdir" + ./spc doctor --auto-fix + ./spc download --with-php="$PHP_SERIES" --for-extensions="$UNIX_EXTENSIONS" --prefer-pre-built + ./spc build --build-cli "$UNIX_EXTENSIONS" + ) + + cp "$workdir/buildroot/bin/php" "$PHP_BINARY" + chmod +x "$PHP_BINARY" + rm -rf "$workdir" +} + +if [ "$PLATFORM" = "win32" ]; then + fetch_windows +else + fetch_unix +fi + +"$PHP_BINARY" -v +echo "PHP runtime installed at $PHP_BINARY" diff --git a/scripts/desktop/smoke-test.sh b/scripts/desktop/smoke-test.sh new file mode 100644 index 000000000..9bce36231 --- /dev/null +++ b/scripts/desktop/smoke-test.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# +# Boots the assembled desktop app distribution (desktop-build/app) with the +# given PHP binary the same way the Electron shell does: verifies required +# extensions, runs migrations, caches config/routes/views, then serves HTTP +# and asserts the health endpoint responds. +# +# Usage: scripts/desktop/smoke-test.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +APP="$ROOT/desktop-build/app" +PHP_BIN="${1:?usage: smoke-test.sh }" + +if [ ! -f "$APP/artisan" ]; then + echo "desktop-build/app is missing. Run scripts/desktop/build-app.sh first." >&2 + exit 1 +fi + +to_php_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + +IS_WINDOWS=false +case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) IS_WINDOWS=true ;; +esac + +BIN_DIR="$(cd "$(dirname "$PHP_BIN")" && pwd)" +PHP=("$PHP_BIN") + +if [ -f "$BIN_DIR/cacert.pem" ]; then + CA_PATH="$(to_php_path "$BIN_DIR/cacert.pem")" + PHP+=(-d "curl.cainfo=$CA_PATH" -d "openssl.cafile=$CA_PATH") +fi + +if [ "$IS_WINDOWS" = "true" ] && [ -d "$BIN_DIR/ext" ]; then + PHP+=(-d "extension_dir=$(to_php_path "$BIN_DIR/ext")") +fi + +"${PHP[@]}" -v + +REQUIRED_EXTENSIONS=(ctype curl dom fileinfo filter ftp iconv intl mbstring openssl pdo_sqlite phar session simplexml sockets sqlite3 tokenizer xml xmlreader xmlwriter zip zlib) +if [ "$IS_WINDOWS" != "true" ]; then + REQUIRED_EXTENSIONS+=(pcntl posix) +fi + +LOADED="$("${PHP[@]}" -m)" +for extension in "${REQUIRED_EXTENSIONS[@]}"; do + if ! grep -qix "$extension" <<< "$LOADED"; then + echo "Bundled PHP is missing required extension: $extension" >&2 + exit 1 + fi +done + +DATA="$(mktemp -d)" +trap 'rm -rf "$DATA"' EXIT + +mkdir -p \ + "$DATA/storage/app/public" \ + "$DATA/storage/framework/cache/data" \ + "$DATA/storage/framework/sessions" \ + "$DATA/storage/framework/views" \ + "$DATA/storage/logs" \ + "$DATA/runtime/cache" + +touch "$DATA/storage/database.sqlite" + +PHP_DATA="$(to_php_path "$DATA")" + +export VITO_DESKTOP=true +export VITO_DATA_PATH="$PHP_DATA" +export VITO_ENV_PATH="$PHP_DATA/runtime" +export VITO_STORAGE_PATH="$PHP_DATA/storage" +export APP_ENV=production +export APP_DEBUG=false +export APP_KEY="base64:$("${PHP[@]}" -r 'echo base64_encode(random_bytes(32));')" +export APP_URL="http://127.0.0.1:18211" +export APP_SERVICES_CACHE="$PHP_DATA/runtime/cache/services.php" +export APP_PACKAGES_CACHE="$PHP_DATA/runtime/cache/packages.php" +export APP_CONFIG_CACHE="$PHP_DATA/runtime/cache/config.php" +export APP_ROUTES_CACHE="$PHP_DATA/runtime/cache/routes.php" +export APP_EVENTS_CACHE="$PHP_DATA/runtime/cache/events.php" +export VIEW_COMPILED_PATH="$PHP_DATA/storage/framework/views" +export DB_CONNECTION=sqlite +export DB_DATABASE=database.sqlite +export QUEUE_CONNECTION=database +export CACHE_DRIVER=file +export SESSION_DRIVER=file +export FILESYSTEM_DISK=local +export INERTIA_ENSURE_PAGES_EXIST=false +export MAIL_MAILER=log +export WS_HOST=127.0.0.1 +export WS_PORT=18212 +export WS_BROADCAST_SECRET=smoke-test-secret + +touch "$DATA/runtime/.env" + +cd "$APP" + +"${PHP[@]}" artisan --version +"${PHP[@]}" artisan migrate --force +"${PHP[@]}" artisan optimize + +ROUTER="$(to_php_path "$APP/vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php")" +(cd "$APP/public" && exec "${PHP[@]}" -S 127.0.0.1:18211 "$ROUTER") & +SERVER_PID=$! +trap 'kill "$SERVER_PID" 2>/dev/null || true; rm -rf "$DATA"' EXIT + +HEALTHY=false +for _ in $(seq 1 30); do + if curl -fsS "http://127.0.0.1:18211/api/health" | grep -q '"success":true'; then + HEALTHY=true + break + fi + sleep 1 +done + +if [ "$HEALTHY" != "true" ]; then + echo "Desktop app HTTP health check failed" >&2 + exit 1 +fi + +curl -fsS "http://127.0.0.1:18211/desktop/login" >/dev/null + +echo "Desktop app smoke test passed" diff --git a/tests/Feature/DesktopAuthTest.php b/tests/Feature/DesktopAuthTest.php new file mode 100644 index 000000000..17689e9b0 --- /dev/null +++ b/tests/Feature/DesktopAuthTest.php @@ -0,0 +1,235 @@ +set('desktop.enabled', false); + + $this->get(route('desktop.login'))->assertNotFound(); + } + + public function test_desktop_login_screen_lists_users(): void + { + config()->set('desktop.enabled', true); + + $this->withoutVite(); + + $this->get(route('desktop.login')) + ->assertOk() + ->assertInertia(fn (AssertableInertia $page) => $page + ->component('auth/desktop-login') + ->where('setup_required', false) + ->where('locked', false) + ->where('locked_user_id', null) + ->has('users', 1) + ->where('users.0.id', $this->user->id) + ); + } + + public function test_desktop_first_run_setup_screen_is_shown_when_no_users_exist(): void + { + config()->set('desktop.enabled', true); + + User::query()->delete(); + + $this->withoutVite(); + + $this->get(route('desktop.login')) + ->assertOk() + ->assertInertia(fn (AssertableInertia $page) => $page + ->component('auth/desktop-login') + ->where('setup_required', true) + ->where('locked', false) + ->where('locked_user_id', null) + ->has('users', 0) + ); + } + + public function test_desktop_first_run_setup_creates_admin_and_logs_in(): void + { + config()->set('desktop.enabled', true); + + User::query()->delete(); + + $response = $this->post(route('desktop.setup'), [ + 'name' => 'Desktop Admin', + 'email' => 'desktop@example.com', + 'password' => 'secret-password', + 'password_confirmation' => 'secret-password', + ]); + + /** @var User $user */ + $user = User::query()->where('email', 'desktop@example.com')->firstOrFail(); + + $this->assertAuthenticatedAs($user); + $this->assertTrue($user->is_admin); + $this->assertSame('UTC', $user->timezone); + $this->assertNotNull($user->current_project_id); + $this->assertDatabaseHas('user_project', [ + 'user_id' => $user->id, + 'project_id' => $user->current_project_id, + ]); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_desktop_first_run_setup_is_unavailable_when_user_exists(): void + { + config()->set('desktop.enabled', true); + + $this->post(route('desktop.setup'), [ + 'name' => 'Second Admin', + 'email' => 'second@example.com', + 'password' => 'secret-password', + 'password_confirmation' => 'secret-password', + ])->assertNotFound(); + + $this->assertDatabaseMissing('users', [ + 'email' => 'second@example.com', + ]); + } + + public function test_desktop_first_run_setup_is_unavailable_outside_desktop_mode(): void + { + config()->set('desktop.enabled', false); + + User::query()->delete(); + + $this->post(route('desktop.setup'), [ + 'name' => 'Desktop Admin', + 'email' => 'desktop@example.com', + 'password' => 'secret-password', + 'password_confirmation' => 'secret-password', + ])->assertNotFound(); + + $this->assertDatabaseMissing('users', [ + 'email' => 'desktop@example.com', + ]); + } + + public function test_desktop_users_can_login_by_clicking_user_before_locking(): void + { + config()->set('desktop.enabled', true); + + $response = $this->post(route('desktop.login.store', $this->user)); + + $this->assertAuthenticatedAs($this->user); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_desktop_lock_logs_out_and_marks_session_locked(): void + { + config()->set('desktop.enabled', true); + + $response = $this->actingAs($this->user)->post(route('desktop.lock')); + + $this->assertGuest(); + $response + ->assertRedirect(route('desktop.login')) + ->assertSessionHas('desktop.locked_user_id', $this->user->id); + } + + public function test_desktop_unlock_requires_password_after_locking(): void + { + config()->set('desktop.enabled', true); + + /** @var User $user */ + $user = User::factory()->create([ + 'password' => Hash::make('secret-password'), + ]); + + $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'wrong-password', + ]) + ->assertSessionHasErrors('password'); + + $this->assertGuest(); + + $response = $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'secret-password', + ]); + + $this->assertAuthenticatedAs($user); + $response + ->assertRedirect(RouteServiceProvider::HOME) + ->assertSessionMissing('desktop.locked_user_id'); + } + + public function test_desktop_unlock_requires_two_factor_code_when_enabled(): void + { + config()->set('desktop.enabled', true); + + $secret = app(Google2FA::class)->generateSecretKey(); + + /** @var User $user */ + $user = User::factory()->create([ + 'password' => Hash::make('secret-password'), + 'two_factor_secret' => encrypt($secret), + 'two_factor_recovery_codes' => encrypt((string) json_encode(['recovery-code-1'])), + 'two_factor_confirmed_at' => now(), + ]); + + $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'secret-password', + ]) + ->assertSessionHasErrors('code'); + + $this->assertGuest(); + + $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'secret-password', + 'code' => '000000', + ]) + ->assertSessionHasErrors('code'); + + $this->assertGuest(); + + $response = $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'secret-password', + 'code' => app(Google2FA::class)->getCurrentOtp($secret), + ]); + + $this->assertAuthenticatedAs($user); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_desktop_unlock_accepts_recovery_code(): void + { + config()->set('desktop.enabled', true); + + /** @var User $user */ + $user = User::factory()->create([ + 'password' => Hash::make('secret-password'), + 'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()), + 'two_factor_recovery_codes' => encrypt((string) json_encode(['recovery-code-1'])), + 'two_factor_confirmed_at' => now(), + ]); + + $response = $this->withSession(['desktop.locked_user_id' => $user->id]) + ->post(route('desktop.login.store', $user), [ + 'password' => 'secret-password', + 'code' => 'recovery-code-1', + ]); + + $this->assertAuthenticatedAs($user); + $response->assertRedirect(RouteServiceProvider::HOME); + $this->assertNotContains('recovery-code-1', $user->fresh()->recoveryCodes()); + } +} diff --git a/tests/Feature/NotificationChannelsTest.php b/tests/Feature/NotificationChannelsTest.php index 72da209a9..2753cb663 100644 --- a/tests/Feature/NotificationChannelsTest.php +++ b/tests/Feature/NotificationChannelsTest.php @@ -274,16 +274,16 @@ public function test_guest_cannot_access_notification_channels(): void ]); $this->get(route('notification-channels')) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->post(route('notification-channels.store'), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->patch(route('notification-channels.update', $channel), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->delete(route('notification-channels.destroy', $channel)) - ->assertRedirect('/'); + ->assertRedirect(route('login')); } public function test_cannot_manipulate_user_id_on_creation(): void diff --git a/tests/Feature/ServerProvidersTest.php b/tests/Feature/ServerProvidersTest.php index 32310f7a2..54a8f5f61 100644 --- a/tests/Feature/ServerProvidersTest.php +++ b/tests/Feature/ServerProvidersTest.php @@ -183,19 +183,19 @@ public function test_guest_cannot_access_server_providers(): void ]); $this->get(route('server-providers')) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->get(route('server-providers.regions', $serverProvider)) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->post(route('server-providers.store'), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->patch(route('server-providers.update', $serverProvider), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->delete(route('server-providers.destroy', $serverProvider)) - ->assertRedirect('/'); + ->assertRedirect(route('login')); } public function test_cannot_manipulate_user_id_on_creation(): void diff --git a/tests/Feature/SourceControlsTest.php b/tests/Feature/SourceControlsTest.php index b175876bc..4eb505c9c 100644 --- a/tests/Feature/SourceControlsTest.php +++ b/tests/Feature/SourceControlsTest.php @@ -191,16 +191,16 @@ public function test_guest_cannot_access_source_controls(): void ]); $this->get(route('source-controls')) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->post(route('source-controls.store'), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->patch(route('source-controls.update', $sourceControl), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->delete(route('source-controls.destroy', $sourceControl)) - ->assertRedirect('/'); + ->assertRedirect(route('login')); } public function test_cannot_manipulate_user_id_on_creation(): void diff --git a/tests/Feature/StorageProvidersTest.php b/tests/Feature/StorageProvidersTest.php index 660628f7f..698478d06 100644 --- a/tests/Feature/StorageProvidersTest.php +++ b/tests/Feature/StorageProvidersTest.php @@ -239,16 +239,16 @@ public function test_guest_cannot_access_storage_providers(): void ]); $this->get(route('storage-providers')) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->post(route('storage-providers.store'), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->patch(route('storage-providers.update', $storageProvider), []) - ->assertRedirect('/'); + ->assertRedirect(route('login')); $this->delete(route('storage-providers.destroy', $storageProvider)) - ->assertRedirect('/'); + ->assertRedirect(route('login')); } public function test_cannot_manipulate_user_id_on_creation(): void diff --git a/tests/Unit/Commands/GenerateKeysCommandTest.php b/tests/Unit/Commands/GenerateKeysCommandTest.php new file mode 100644 index 000000000..4632be9ae --- /dev/null +++ b/tests/Unit/Commands/GenerateKeysCommandTest.php @@ -0,0 +1,31 @@ +app->useStoragePath($storagePath); + + $this->artisan('ssh-key:generate', ['--force' => true]) + ->assertSuccessful(); + + $this->assertFileExists($storagePath.'/ssh-private.pem'); + $this->assertFileExists($storagePath.'/ssh-public.key'); + $this->assertNotEmpty(file_get_contents($storagePath.'/ssh-public.key')); + + File::deleteDirectory($storagePath); + } +} diff --git a/tests/Unit/DesktopRuntimeTest.php b/tests/Unit/DesktopRuntimeTest.php new file mode 100644 index 000000000..dcb5fcce9 --- /dev/null +++ b/tests/Unit/DesktopRuntimeTest.php @@ -0,0 +1,39 @@ +set('app.url', 'http://127.0.0.1:47291'); + config()->set('app.ws_url', null); + config()->set('core.ws_port', 48125); + config()->set('desktop.enabled', true); + + $this->assertSame('ws://127.0.0.1:48125/ws/events', DesktopRuntime::websocketUrl('/ws/events')); + } + + public function test_builds_server_websocket_url_without_desktop_port(): void + { + config()->set('app.url', 'https://vito.example.com'); + config()->set('app.ws_url', null); + config()->set('core.ws_port', 48125); + config()->set('desktop.enabled', false); + + $this->assertSame('wss://vito.example.com/ws/events', DesktopRuntime::websocketUrl('/ws/events')); + } + + public function test_joins_desktop_storage_paths(): void + { + config()->set('desktop.storage_path', '/tmp/vito/storage'); + + $this->assertSame('/tmp/vito/storage/framework/views', DesktopRuntime::storagePath('framework/views')); + } +} diff --git a/vite.config.ts b/vite.config.ts index 17ddb32a1..d98dc565e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,12 +5,13 @@ import laravel from 'laravel-vite-plugin'; import { resolve } from 'node:path'; import { defineConfig } from 'vite'; -export default defineConfig({ +export default defineConfig(({ mode }) => ({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], ssr: 'resources/js/ssr.tsx', refresh: true, + buildDirectory: mode === 'desktop' ? 'build-desktop' : 'build', }), inertia(), react(), @@ -25,4 +26,4 @@ export default defineConfig({ 'decimal.js-light': resolve(__dirname, 'node_modules/decimal.js-light/decimal.mjs'), }, }, -}); +}));