diff --git a/extensions/webapp-manager/.gitignore b/extensions/webapp-manager/.gitignore new file mode 100644 index 00000000..eb3ae205 --- /dev/null +++ b/extensions/webapp-manager/.gitignore @@ -0,0 +1,2 @@ +node_modules +vicinae-env.d.ts diff --git a/extensions/webapp-manager/README.md b/extensions/webapp-manager/README.md new file mode 100644 index 00000000..220c05a5 --- /dev/null +++ b/extensions/webapp-manager/README.md @@ -0,0 +1,292 @@ +# Manage Webapps + +Create, edit, launch, and remove Linux web app `.desktop` entries from Vicinae. + +This extension is intended for users who pin websites as desktop apps, usually with Chromium-style app windows such as `--app=https://example.com`. It can also keep one window per web app by focusing an existing window before launching another one. + +## Features + +- Create, edit, delete, search, and launch managed web app desktop entries +- Write `.desktop` files to a configurable applications directory +- Use configurable browser commands and argument templates +- Download favicons for launcher icons through DuckDuckGo's favicon service +- Convert downloaded WebP favicons to PNG when needed +- Export and import managed web app configuration as JSON +- Refresh one favicon or every managed favicon +- Optional single-window behavior that focuses an existing app window before launching +- Automatic window match learning on first launch +- Optional generated global shortcut configuration for supported window managers + +## Supported Window Managers + +Single-window focus support is available for: + +- `niri` +- `hyprland` +- `sway` +- `i3` +- `custom` + +Generated global shortcut installation is available for: + +- `niri` +- `hyprland` +- `sway` +- `i3` + +The `custom` backend can run a user-provided focus command, but it does not install global shortcuts automatically. + +## Requirements + +- Linux desktop environment +- Vicinae +- Node.js and npm for local development +- `jq` for automatic window detection and single-window focus behavior +- One of the supported window manager CLIs when using a built-in backend: + - `niri` + - `hyprctl` + - `swaymsg` + - `i3-msg` + +Browser defaults are optimized for Chromium-compatible browsers, but each entry can override the browser command and arguments. + +## Installation + +Install from the Vicinae store once this extension has been published. + +For local development or manual installation: + +```bash +npm install +npm run build +``` + +The `postbuild` script copies `@cwasm/webp`'s `webp.wasm` file into Vicinae's local extension directory so favicon conversion works at runtime. + +For development mode: + +```bash +npm run dev +``` + +Then open Vicinae and run `Manage Webapps`. + +## Preferences + +Configure these in Vicinae extension preferences: + +| Preference | Default | Description | +| --- | --- | --- | +| Desktop Entries Directory | `~/.local/share/applications` | Directory where managed `.desktop` files are written | +| Browser Command | `chromium-browser` | Browser executable or command prefix | +| Browser Args Template | `--app={url}` | Browser arguments used by new entries | +| Window Manager | `niri` | Backend used for focusing existing windows | +| Custom Focus Command | empty | Command used only when Window Manager is `custom` | + +Browser argument templates support: + +- `{url}` +- `{origin}` +- `{hostname}` + +Examples: + +```text +--app={url} +--new-window {url} +--profile-directory=Default --app={url} +``` + +Custom focus commands support: + +- `{match}` +- `{mode}` + +The custom command should focus a matching window and exit `0` on success. It should exit non-zero when no window matched. + +## Usage + +Run `Manage Webapps` in Vicinae. + +Use `Shift+Enter` or `Create Desktop Entry` to add a web app. Fill in the entry name, URL, optional comment, optional global shortcut, browser command, browser arguments, and single-window settings. + +Managed files are named like `vicinae-.desktop` and include `X-Vicinae-*` metadata so the extension only manages entries it created. + +Useful actions: + +- `Edit Desktop Entry`: update an existing entry +- `Open Webapp`: run the generated launcher script +- `Refresh Favicon`: refetch the selected entry favicon +- `Refresh All Favicons`: refetch every managed favicon and refresh shortcut files +- `Export Webapp Config`: write and copy JSON backup data +- `Import Webapp Config`: import exported JSON from a file or pasted text +- `Install Global Shortcuts`: write window manager shortcut config +- `Show Desktop File`: open the generated `.desktop` file location +- `Delete Desktop Entry`: remove the desktop entry, icon, launcher script, and window state files + +## Single-Window Mode + +Each entry can enable `Reuse and focus existing window if already open`. + +Match strategies: + +- `App ID` +- `Class` +- `Title` +- `Any Field` + +On first launch, the generated launcher script records current windows, launches the app, waits for the new window, detects the selected match value, and stores it under the extension support directory. Later launches try to focus a matching existing window before starting a new browser process. + +If focus behavior is unreliable for a specific browser or site, edit the entry and try a different match strategy. + +## Global Shortcuts + +Set `Global Shortcut` on entries with values such as: + +```text +ctrl+shift+g +alt+space +super+b +``` + +Supported modifiers include `ctrl`, `shift`, `alt` or `opt`, and `super` or `cmd`. + +Run `Install Global Shortcuts` after adding or changing shortcuts. Saving or deleting an entry also refreshes generated shortcut configuration. + +Generated files: + +| Window manager | Generated file | Main config update | +| --- | --- | --- | +| `niri` | `~/.config/niri/vicinae-webapps.kdl` | Adds `include "vicinae-webapps.kdl"` to `~/.config/niri/config.kdl` | +| `hyprland` | `~/.config/hypr/vicinae-webapps.conf` | Adds `source = ~/.config/hypr/vicinae-webapps.conf` to `~/.config/hypr/hyprland.conf` | +| `sway` | `~/.config/sway/vicinae-webapps.conf` | Adds `include ~/.config/sway/vicinae-webapps.conf` to `~/.config/sway/config` | +| `i3` | `~/.config/i3/vicinae-webapps.conf` | Adds `include ~/.config/i3/vicinae-webapps.conf` to `~/.config/i3/config` | + +Hyprland, Sway, and i3 configs are reloaded on a best-effort basis after installation. + +## Export and Import + +`Export Webapp Config` writes `webapp-config-export.json` under the Vicinae extension support directory and copies the JSON to the clipboard. + +`Import Webapp Config` accepts either that JSON file or pasted JSON. Existing entries are updated when the exported ID matches, or when the same name and URL already exist. + +The export format is: + +```json +{ + "schema": "vicinae-webapp-config", + "version": 1, + "exportedAt": "2026-05-27T00:00:00.000Z", + "entries": [] +} +``` + +## Project Structure + +```text +. +|-- assets/extension_icon.png +|-- scripts/copy-webp-wasm.cjs +|-- src/manage-desktop-entries.tsx +|-- src/lib/desktop-entry-manager.ts +|-- package.json +|-- package-lock.json +`-- tsconfig.json +``` + +Key files: + +- `package.json`: Vicinae manifest, command declaration, preferences, scripts, and dependencies +- `src/manage-desktop-entries.tsx`: Vicinae UI, actions, import/export, shortcut config generation +- `src/lib/desktop-entry-manager.ts`: desktop file parsing/writing, launcher script generation, favicon handling +- `scripts/copy-webp-wasm.cjs`: copies the WebP WASM runtime after build +- `assets/extension_icon.png`: extension icon used by the manifest + +## Development + +Install dependencies: + +```bash +npm install +``` + +Run in development mode: + +```bash +npm run dev +``` + +Build: + +```bash +npm run build +``` + +Format: + +```bash +npm run format +``` + +Lint and validate the Vicinae extension manifest: + +```bash +npm run lint +``` + +## Publishing to `vicinaehq/extensions` + +The upstream repository expects store extensions under `extensions//`. For this extension, the target directory is: + +```text +extensions/webapp-manager/ +``` + +PR preparation checklist: + +- Place this package at `extensions/webapp-manager/` +- Keep `package-lock.json` committed +- Keep `assets/extension_icon.png` committed +- Run `npm install` if dependencies changed +- Run `npm run build` +- Run `npm run lint` +- Confirm the extension opens in Vicinae and the `Manage Webapps` command loads +- In the PR description, mention that the extension writes `.desktop` files, generated launcher scripts, generated window manager shortcut files, and favicon image files + +## Troubleshooting + +### Entry launches but does not focus an existing window + +- Ensure `jq` is installed +- Ensure the selected window manager CLI exists +- Launch once after enabling single-window mode so the match value can be learned +- Try a different match strategy in the entry form + +### Desktop icon is generic or missing + +- Use `Refresh Favicon` +- Use `Refresh All Favicons` +- Check network access to `https://icons.duckduckgo.com/ip3/.ico` +- If favicon download fails, the entry falls back to `web-browser` + +### Desktop entry does not appear in the launcher menu + +Some environments need the desktop application database refreshed manually: + +```bash +update-desktop-database ~/.local/share/applications +``` + +### Global shortcut installation fails + +- Confirm the selected window manager is `niri`, `hyprland`, `sway`, or `i3` +- Confirm the main window manager config file exists +- Check for duplicate shortcuts across entries +- Re-run `Install Global Shortcuts` + +## Notes + +- The extension only lists `.desktop` files marked with `X-Vicinae-Managed=true` +- Deleting an entry removes its generated desktop file, icon, launcher script, and learned window state +- Favicon downloads are best effort and do not block entry creation +- Existing entries can be edited and saved again to regenerate launcher behavior diff --git a/extensions/webapp-manager/assets/extension_icon.png b/extensions/webapp-manager/assets/extension_icon.png new file mode 100644 index 00000000..3e1ed0be Binary files /dev/null and b/extensions/webapp-manager/assets/extension_icon.png differ diff --git a/extensions/webapp-manager/package-lock.json b/extensions/webapp-manager/package-lock.json new file mode 100644 index 00000000..7c6eb9db --- /dev/null +++ b/extensions/webapp-manager/package-lock.json @@ -0,0 +1,3918 @@ +{ + "name": "webapp-manager", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webapp-manager", + "license": "MIT", + "dependencies": { + "@cwasm/webp": "^0.1.5", + "@vicinae/api": "^0.19.7", + "fast-png": "^8.0.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.2", + "@biomejs/cli-darwin-x64": "2.3.2", + "@biomejs/cli-linux-arm64": "2.3.2", + "@biomejs/cli-linux-arm64-musl": "2.3.2", + "@biomejs/cli-linux-x64": "2.3.2", + "@biomejs/cli-linux-x64-musl": "2.3.2", + "@biomejs/cli-win32-arm64": "2.3.2", + "@biomejs/cli-win32-x64": "2.3.2" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", + "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", + "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", + "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", + "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", + "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", + "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@canvas/image-data": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz", + "integrity": "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==", + "license": "MIT" + }, + "node_modules/@cwasm/webp": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@cwasm/webp/-/webp-0.1.5.tgz", + "integrity": "sha512-ceIZQkyxK+s7mmItNcWqqHdOBiJAxYxTnrnPNgUNjldB1M9j+Bp/3eVIVwC8rUFyN/zoFwuT0331pyY3ackaNA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jgoz/esbuild-plugin-typecheck": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@jgoz/esbuild-plugin-typecheck/-/esbuild-plugin-typecheck-4.0.4.tgz", + "integrity": "sha512-ca38NAWnE/GchWjO5m7Wbny+yMOsYkoJOboQGheCjnnu5uDxqQWJSIegN+C+CWl8K/1naI/cMfTrAfDH1oRoVQ==", + "license": "MIT", + "peerDependencies": { + "@jgoz/esbuild-plugin-livereload": ">=2.1.4", + "esbuild": ">=0.25.0", + "typescript": ">= 3.5" + }, + "peerDependenciesMeta": { + "@jgoz/esbuild-plugin-livereload": { + "optional": true + } + } + }, + "node_modules/@oclif/core": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.8.0.tgz", + "integrity": "sha512-jteNUQKgJHLHFbbz806aGZqf+RJJ7t4gwF4MYa8fCwCxQ8/klJNWc0MvaJiBebk7Mc+J39mdlsB4XraaCKznFw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.17.0", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.4.3", + "ejs": "^3.1.10", + "get-package-type": "^0.1.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "lilconfig": "^3.1.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.14", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.37", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.37.tgz", + "integrity": "sha512-5N/X/FzlJaYfpaHwDC0YHzOzKDWa41s9t+4FpCDu4f9OMReds4JeNBaaWk9rlIzdKjh2M6AC5Q18ORfECRkHGA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-plugins": { + "version": "5.4.56", + "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-5.4.56.tgz", + "integrity": "sha512-mZjRudlmVSr6Stz0CVFuaIZOjwZ5DqjWepQCR/yK9nbs8YunGautpuxBx/CcqaEH29xiQfsuNOIUWa1w/+3VSA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.8.0", + "ansis": "^3.17.0", + "debug": "^4.4.0", + "npm": "^10.9.4", + "npm-package-arg": "^11.0.3", + "npm-run-path": "^5.3.0", + "object-treeify": "^4.0.1", + "semver": "^7.7.4", + "validate-npm-package-name": "^5.0.1", + "which": "^4.0.0", + "yarn": "^1.22.22" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@vicinae/api": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@vicinae/api/-/api-0.19.9.tgz", + "integrity": "sha512-7HSvOg6yorL3PAaVckQRvbVuihGvyE9ViSNFTGltXQQDv1jrGHpcs4ividK/evCHqs8iaYz3w2DFuF7uYJ/PQw==", + "license": "ISC", + "dependencies": { + "@jgoz/esbuild-plugin-typecheck": "^4.0.3", + "@oclif/core": "^4", + "@oclif/plugin-help": "^6", + "@oclif/plugin-plugins": "^5", + "@types/node": ">=18", + "@types/react": "19.0.10", + "chokidar": "^4.0.3", + "esbuild": "^0.25.2", + "react": "19.0.0", + "zod": "^4.0.17" + }, + "bin": { + "vici": "bin/run.js" + }, + "peerDependencies": { + "@types/node": ">=18", + "@types/react": "19.0.10" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-png": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-8.0.0.tgz", + "integrity": "sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.2", + "iobuffer": "^6.0.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iobuffer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-6.0.1.tgz", + "integrity": "sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/npm": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.4.tgz", + "integrity": "sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^8.0.1", + "@npmcli/config": "^9.0.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.2.0", + "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/redact": "^3.2.2", + "@npmcli/run-script": "^9.1.0", + "@sigstore/tuf": "^3.1.1", + "abbrev": "^3.0.1", + "archy": "~1.0.0", + "cacache": "^19.0.1", + "chalk": "^5.4.1", + "ci-info": "^4.2.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.4.5", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^8.1.0", + "ini": "^5.0.0", + "init-package-json": "^7.0.2", + "is-cidr": "^5.1.1", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^9.0.0", + "libnpmdiff": "^7.0.1", + "libnpmexec": "^9.0.1", + "libnpmfund": "^6.0.1", + "libnpmhook": "^11.0.0", + "libnpmorg": "^7.0.0", + "libnpmpack": "^8.0.1", + "libnpmpublish": "^10.0.1", + "libnpmsearch": "^8.0.0", + "libnpmteam": "^7.0.0", + "libnpmversion": "^7.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.5", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.2.0", + "nopt": "^8.1.0", + "normalize-package-data": "^7.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.1", + "npm-package-arg": "^12.0.2", + "npm-pick-manifest": "^10.0.0", + "npm-profile": "^11.0.1", + "npm-registry-fetch": "^18.0.2", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^19.0.1", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.1.0", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^9.4.0", + "tar": "^6.2.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.1", + "which": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^8.0.0", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.1", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "bin-links": "^5.0.0", + "cacache": "^19.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^19.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "6.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^19.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^20.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { + "version": "20.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.2.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "19.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.3", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.4.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^6.0.0", + "npm-package-arg": "^12.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "7.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^2.3.0", + "diff": "^5.1.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "tar": "^6.2.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/run-script": "^9.0.1", + "ci-info": "^4.0.0", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "proc-log": "^5.0.0", + "read": "^4.0.0", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "11.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "10.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^3.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.4.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "14.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "12.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "11.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "19.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/bundle": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/core": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/sign": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/verify": { + "version": "2.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.21", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sprintf-js": { + "version": "1.1.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": 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/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "inBundle": 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/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.14", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "inBundle": 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/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/object-treeify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-4.0.1.tgz", + "integrity": "sha512-Y6tg5rHfsefSkfKujv2SwHulInROy/rCL5F4w0QOWxut8AnxYxf0YmNhTh95Zfyxpsudo66uqkux0ACFnyMSgQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "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==", + "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/yarn": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz", + "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==", + "hasInstallScript": true, + "license": "BSD-2-Clause", + "bin": { + "yarn": "bin/yarn.js", + "yarnpkg": "bin/yarn.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/extensions/webapp-manager/package.json b/extensions/webapp-manager/package.json new file mode 100644 index 00000000..924f3f63 --- /dev/null +++ b/extensions/webapp-manager/package.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json", + "name": "webapp-manager", + "title": "Manage Webapps Extension", + "description": "Create and manage desktop entries that launch URLs with configurable browser commands.", + "categories": ["Web", "Productivity"], + "license": "MIT", + "author": "Xyz3R", + "contributors": [], + "pastContributors": [], + "icon": "extension_icon.png", + "commands": [ + { + "name": "manage-desktop-entries", + "title": "Manage Webapps", + "subtitle": "Create, edit, and delete URL desktop entries", + "description": "Manage .desktop entries for URLs and configure browser launch arguments.", + "mode": "view" + } + ], + "preferences": [ + { + "type": "directory", + "name": "desktopEntryDirectory", + "title": "Desktop Entries Directory", + "description": "Where managed .desktop files are written.", + "required": true, + "default": "~/.local/share/applications" + }, + { + "type": "textfield", + "name": "browserCommand", + "title": "Browser Command", + "description": "Command used to launch web apps from desktop entries.", + "required": true, + "placeholder": "chromium-browser", + "default": "chromium-browser" + }, + { + "type": "textfield", + "name": "browserArgsTemplate", + "title": "Browser Args Template", + "description": "Arguments template for the browser command. Supports {url}, {origin}, and {hostname}.", + "required": true, + "placeholder": "--app={url}", + "default": "--app={url}" + }, + { + "type": "dropdown", + "name": "windowManager", + "title": "Window Manager", + "description": "Window manager backend used to focus existing windows for single-window entries.", + "required": true, + "default": "niri", + "data": [ + { + "title": "niri", + "value": "niri" + }, + { + "title": "Hyprland", + "value": "hyprland" + }, + { + "title": "Sway", + "value": "sway" + }, + { + "title": "i3", + "value": "i3" + }, + { + "title": "Custom", + "value": "custom" + } + ] + }, + { + "type": "textfield", + "name": "customWindowFocusCommand", + "title": "Custom Focus Command", + "description": "Used only when Window Manager is custom. Command should focus an existing window and exit 0 on success. Supports {match} and {mode}.", + "required": false, + "placeholder": "my-wm-focus --mode {mode} --match {match}" + } + ], + "scripts": { + "build": "vici build", + "postbuild": "node scripts/copy-webp-wasm.cjs", + "dev": "vici develop", + "format": "biome format --write src", + "lint": "vici lint" + }, + "dependencies": { + "@cwasm/webp": "^0.1.5", + "@vicinae/api": "^0.19.7", + "fast-png": "^8.0.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "typescript": "^5.9.2" + } +} diff --git a/extensions/webapp-manager/scripts/copy-webp-wasm.cjs b/extensions/webapp-manager/scripts/copy-webp-wasm.cjs new file mode 100644 index 00000000..38069fe0 --- /dev/null +++ b/extensions/webapp-manager/scripts/copy-webp-wasm.cjs @@ -0,0 +1,22 @@ +const fs = require("node:fs/promises"); +const os = require("node:os"); +const path = require("node:path"); + +async function main() { + const packageJson = require("../package.json"); + const sourcePath = require.resolve("@cwasm/webp/webp.wasm"); + const extensionDirectory = path.join( + os.homedir(), + ".local/share/vicinae/extensions", + packageJson.name, + ); + const targetPath = path.join(extensionDirectory, "webp.wasm"); + + await fs.mkdir(extensionDirectory, { recursive: true }); + await fs.copyFile(sourcePath, targetPath); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); \ No newline at end of file diff --git a/extensions/webapp-manager/src/lib/desktop-entry-manager.ts b/extensions/webapp-manager/src/lib/desktop-entry-manager.ts new file mode 100644 index 00000000..8e989ac5 --- /dev/null +++ b/extensions/webapp-manager/src/lib/desktop-entry-manager.ts @@ -0,0 +1,1147 @@ +import { randomUUID } from "node:crypto"; +import { constants as fsConstants } from "node:fs"; +import * as fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { decode as decodeWebp } from "@cwasm/webp"; +import { encode as encodePng } from "fast-png"; + +export type WindowManager = "niri" | "hyprland" | "sway" | "i3" | "custom"; +export type WindowMatchMode = "app-id" | "class" | "title" | "any"; + +export type ManagedDesktopEntry = { + id: string; + name: string; + url: string; + comment?: string; + shortcut?: string; + browserCommand: string; + browserArgsTemplate: string; + singleWindow: boolean; + windowMatchMode: WindowMatchMode; + windowMatchValue?: string; + windowManager: WindowManager; + customFocusCommandTemplate?: string; + desktopFilePath: string; + desktopFileName: string; + launcherScriptPath?: string; + icon?: string; + updatedAt?: Date; +}; + +export type EntryDraft = { + name: string; + url: string; + comment?: string; + shortcut?: string; + browserCommand: string; + browserArgsTemplate: string; + singleWindow: boolean; + windowMatchMode: WindowMatchMode; + downloadFavicon: boolean; +}; + +type SaveEntryOptions = { + directory: string; + iconDirectory: string; + launcherDirectory: string; + stateDirectory: string; + windowManager: WindowManager; + customFocusCommandTemplate?: string; + draft: EntryDraft; + existingEntry?: ManagedDesktopEntry; +}; + +const MANAGED_MARKER_KEY = "X-Vicinae-Managed"; +const MANAGED_MARKER_VALUE = "true"; +const MANAGED_ID_KEY = "X-Vicinae-EntryID"; +const MANAGED_URL_KEY = "X-Vicinae-URL"; +const MANAGED_SHORTCUT_KEY = "X-Vicinae-Shortcut"; +const MANAGED_COMMAND_KEY = "X-Vicinae-BrowserCommand"; +const MANAGED_ARGS_KEY = "X-Vicinae-BrowserArgsTemplate"; +const MANAGED_SINGLE_WINDOW_KEY = "X-Vicinae-SingleWindow"; +const MANAGED_MATCH_MODE_KEY = "X-Vicinae-WindowMatchMode"; +const MANAGED_MATCH_VALUE_KEY = "X-Vicinae-WindowMatchValue"; +const MANAGED_WM_KEY = "X-Vicinae-WindowManager"; +const MANAGED_CUSTOM_FOCUS_KEY = "X-Vicinae-CustomFocusCommand"; +const MANAGED_LAUNCHER_KEY = "X-Vicinae-LauncherScript"; +const MANAGED_STATE_PREFIX_KEY = "X-Vicinae-WindowStatePrefix"; +const MANAGED_VERSION_KEY = "X-Vicinae-Version"; +const MANAGED_VERSION_VALUE = "3"; + +const WINDOW_MANAGERS: WindowManager[] = [ + "niri", + "hyprland", + "sway", + "i3", + "custom", +]; +const WINDOW_MATCH_MODES: WindowMatchMode[] = [ + "app-id", + "class", + "title", + "any", +]; + +export const DEFAULT_DESKTOP_DIRECTORY = "~/.local/share/applications"; +export const DEFAULT_BROWSER_COMMAND = "chromium-browser"; +export const DEFAULT_BROWSER_ARGS_TEMPLATE = "--app={url}"; +export const DEFAULT_WINDOW_MANAGER: WindowManager = "niri"; +export const DEFAULT_WINDOW_MATCH_MODE: WindowMatchMode = "app-id"; + +export function resolvePath(inputPath: string): string { + const trimmed = inputPath.trim(); + if (trimmed === "~") { + return os.homedir(); + } + if (trimmed.startsWith("~/")) { + return path.join(os.homedir(), trimmed.slice(2)); + } + return trimmed; +} + +export function parseWindowManager(value: unknown): WindowManager { + if ( + typeof value === "string" && + WINDOW_MANAGERS.includes(value as WindowManager) + ) { + return value as WindowManager; + } + return DEFAULT_WINDOW_MANAGER; +} + +export function parseWindowMatchMode(value: unknown): WindowMatchMode { + if ( + typeof value === "string" && + WINDOW_MATCH_MODES.includes(value as WindowMatchMode) + ) { + return value as WindowMatchMode; + } + return DEFAULT_WINDOW_MATCH_MODE; +} + +export async function listManagedEntries( + directory: string, +): Promise { + await fs.mkdir(directory, { recursive: true }); + const files = await fs.readdir(directory, { withFileTypes: true }); + const entries = await Promise.all( + files + .filter((file) => file.isFile() && file.name.endsWith(".desktop")) + .map((file) => + parseManagedEntry(path.join(directory, file.name)).catch( + () => undefined, + ), + ), + ); + + return entries + .filter((entry): entry is ManagedDesktopEntry => Boolean(entry)) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +export async function saveManagedEntry( + options: SaveEntryOptions, +): Promise { + const directory = resolvePath(options.directory || DEFAULT_DESKTOP_DIRECTORY); + await fs.mkdir(directory, { recursive: true }); + await fs.mkdir(options.iconDirectory, { recursive: true }); + await fs.mkdir(options.launcherDirectory, { recursive: true }); + await fs.mkdir(options.stateDirectory, { recursive: true }); + + const normalizedUrl = normalizeUrl(options.draft.url); + const browserCommand = options.draft.browserCommand.trim(); + if (!browserCommand) { + throw new Error("Browser command cannot be empty."); + } + + const browserArgsTemplate = + options.draft.browserArgsTemplate.trim() || DEFAULT_BROWSER_ARGS_TEMPLATE; + const shortcut = options.draft.shortcut?.trim() || undefined; + const singleWindow = options.draft.singleWindow; + const windowMatchMode = parseWindowMatchMode(options.draft.windowMatchMode); + const windowManager = parseWindowManager(options.windowManager); + const customFocusCommandTemplate = + options.customFocusCommandTemplate?.trim() || undefined; + + const id = options.existingEntry?.id ?? randomUUID(); + const desktopFileName = + options.existingEntry?.desktopFileName ?? + (await getUniqueDesktopFileName( + directory, + slugifyName(options.draft.name), + )); + const desktopFilePath = path.join(directory, desktopFileName); + const launcherScriptPath = path.join(options.launcherDirectory, `${id}.sh`); + const statePrefix = path.join(options.stateDirectory, id); + + let icon = options.existingEntry?.icon; + if (options.draft.downloadFavicon) { + const downloadedIcon = await downloadFavicon( + normalizedUrl, + id, + options.iconDirectory, + ); + if (downloadedIcon) { + icon = downloadedIcon; + } + } + + const launchTokens = buildLaunchCommandTokens( + browserCommand, + browserArgsTemplate, + normalizedUrl, + ); + await writeLauncherScript({ + launcherScriptPath, + launchTokens, + singleWindow, + windowMatchMode, + windowManager, + customFocusCommandTemplate: customFocusCommandTemplate || "", + statePrefix, + }); + + if (singleWindow && options.existingEntry?.windowMatchValue?.trim()) { + const legacyValue = options.existingEntry.windowMatchValue.trim(); + const stateFile = `${statePrefix}.${windowMatchMode}`; + try { + await fs.access(stateFile, fsConstants.F_OK); + } catch { + await fs.writeFile(stateFile, legacyValue, "utf8"); + } + } + + const lines = [ + "[Desktop Entry]", + "Version=1.0", + "Type=Application", + `Name=${sanitizeDesktopValue(options.draft.name)}`, + options.draft.comment?.trim() + ? `Comment=${sanitizeDesktopValue(options.draft.comment.trim())}` + : undefined, + `Exec=${quoteExecToken(launcherScriptPath)}`, + `Icon=${sanitizeDesktopValue(icon || "web-browser")}`, + "Terminal=false", + "StartupNotify=true", + "Categories=Network;", + `${MANAGED_MARKER_KEY}=${MANAGED_MARKER_VALUE}`, + `${MANAGED_VERSION_KEY}=${MANAGED_VERSION_VALUE}`, + `${MANAGED_ID_KEY}=${sanitizeDesktopValue(id)}`, + `${MANAGED_URL_KEY}=${sanitizeDesktopValue(normalizedUrl)}`, + shortcut + ? `${MANAGED_SHORTCUT_KEY}=${sanitizeDesktopValue(shortcut)}` + : undefined, + `${MANAGED_COMMAND_KEY}=${sanitizeDesktopValue(browserCommand)}`, + `${MANAGED_ARGS_KEY}=${sanitizeDesktopValue(browserArgsTemplate)}`, + `${MANAGED_SINGLE_WINDOW_KEY}=${singleWindow ? "true" : "false"}`, + `${MANAGED_MATCH_MODE_KEY}=${windowMatchMode}`, + `${MANAGED_WM_KEY}=${windowManager}`, + customFocusCommandTemplate + ? `${MANAGED_CUSTOM_FOCUS_KEY}=${sanitizeDesktopValue(customFocusCommandTemplate)}` + : undefined, + `${MANAGED_LAUNCHER_KEY}=${sanitizeDesktopValue(launcherScriptPath)}`, + `${MANAGED_STATE_PREFIX_KEY}=${sanitizeDesktopValue(statePrefix)}`, + ].filter((line): line is string => Boolean(line)); + + await fs.writeFile(desktopFilePath, `${lines.join("\n")}\n`, "utf8"); + await fs.chmod(desktopFilePath, 0o755).catch(() => { + /* best effort */ + }); + + return { + id, + name: options.draft.name.trim(), + url: normalizedUrl, + comment: options.draft.comment?.trim() || undefined, + shortcut, + browserCommand, + browserArgsTemplate, + singleWindow, + windowMatchMode, + windowManager, + customFocusCommandTemplate, + desktopFilePath, + desktopFileName, + launcherScriptPath, + icon, + updatedAt: new Date(), + }; +} + +export async function deleteManagedEntry( + entry: ManagedDesktopEntry, + iconDirectory: string, + launcherDirectory: string, + stateDirectory: string, +): Promise { + await fs + .unlink(entry.desktopFilePath) + .catch((error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") { + throw error; + } + }); + + await removeOldIcons(entry.id, iconDirectory); + + const launcherPath = + entry.launcherScriptPath || path.join(launcherDirectory, `${entry.id}.sh`); + await fs.unlink(launcherPath).catch((error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") { + throw error; + } + }); + + await removeStateFiles(entry.id, stateDirectory); +} + +export function launchScriptPathForEntry( + entry: ManagedDesktopEntry, + launcherDirectory: string, +): string { + return ( + entry.launcherScriptPath || path.join(launcherDirectory, `${entry.id}.sh`) + ); +} + +function normalizeUrl(input: string): string { + const trimmed = input.trim(); + if (!trimmed) { + throw new Error("URL is required."); + } + const withScheme = /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(trimmed) + ? trimmed + : `https://${trimmed}`; + const parsed = new URL(withScheme); + if (!parsed.hostname) { + throw new Error("URL must include a valid host."); + } + return parsed.toString(); +} + +function slugifyName(input: string): string { + const normalized = input + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, ""); + return normalized || "web-app"; +} + +async function getUniqueDesktopFileName( + directory: string, + slug: string, +): Promise { + const base = `vicinae-${slug}`; + for (let index = 1; index < 5000; index += 1) { + const suffix = index === 1 ? "" : `-${index}`; + const candidate = `${base}${suffix}.desktop`; + try { + await fs.access(path.join(directory, candidate), fsConstants.F_OK); + } catch { + return candidate; + } + } + throw new Error("Could not allocate a unique desktop file name."); +} + +function buildLaunchCommandTokens( + browserCommand: string, + argsTemplate: string, + url: string, +): string[] { + const urlData = new URL(url); + const interpolatedArgs = argsTemplate + .replaceAll("{url}", url) + .replaceAll("{origin}", urlData.origin) + .replaceAll("{hostname}", urlData.hostname); + + const commandTokens = splitCommandLine(browserCommand); + if (commandTokens.length === 0) { + throw new Error("Browser command is invalid."); + } + + const argTokens = splitCommandLine(interpolatedArgs); + return [...commandTokens, ...argTokens]; +} + +function splitCommandLine(input: string): string[] { + const args: string[] = []; + let current = ""; + let quote: "single" | "double" | null = null; + let escaped = false; + + for (const char of input.trim()) { + if (escaped) { + current += char; + escaped = false; + continue; + } + + if (char === "\\" && quote !== "single") { + escaped = true; + continue; + } + + if (quote === "single") { + if (char === "'") { + quote = null; + } else { + current += char; + } + continue; + } + + if (quote === "double") { + if (char === '"') { + quote = null; + } else { + current += char; + } + continue; + } + + if (char === "'") { + quote = "single"; + continue; + } + + if (char === '"') { + quote = "double"; + continue; + } + + if (/\s/.test(char)) { + if (current.length > 0) { + args.push(current); + current = ""; + } + continue; + } + + current += char; + } + + if (escaped) { + current += "\\"; + } + + if (quote !== null) { + throw new Error("Unclosed quote in command or args template."); + } + + if (current.length > 0) { + args.push(current); + } + + return args; +} + +function quoteExecToken(token: string): string { + const escapedPercent = token.replace(/%/g, "%%"); + const escaped = escapedPercent + .replace(/\\/g, "\\\\") + .replace(/\$/g, "\\$") + .replace(/`/g, "\\`") + .replace(/"/g, '\\"'); + + if (escaped.length === 0) { + return '""'; + } + + return /\s/.test(escaped) ? `"${escaped}"` : escaped; +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'"'"'`)}'`; +} + +async function writeLauncherScript(options: { + launcherScriptPath: string; + launchTokens: string[]; + singleWindow: boolean; + windowMatchMode: WindowMatchMode; + windowManager: WindowManager; + customFocusCommandTemplate: string; + statePrefix: string; +}): Promise { + const launchTokens = options.launchTokens + .map((token) => shellQuote(token)) + .join(" "); + const content = `#!/usr/bin/env bash +set -u + +SINGLE_WINDOW=${options.singleWindow ? "1" : "0"} +FORCE_SINGLE_WINDOW="\${VICINAE_FORCE_SINGLE_WINDOW:-0}" +WINDOW_MANAGER=${shellQuote(options.windowManager)} +MATCH_MODE=${shellQuote(options.windowMatchMode)} +CUSTOM_FOCUS_COMMAND_TEMPLATE=${shellQuote(options.customFocusCommandTemplate)} +STATE_PREFIX=${shellQuote(options.statePrefix)} + +state_file_for_mode() { + printf '%s.%s' "$STATE_PREFIX" "$MATCH_MODE" +} + +load_match_value() { + local state_file + state_file="$(state_file_for_mode)" + [ -s "$state_file" ] || return 1 + MATCH_VALUE="$(cat "$state_file" 2>/dev/null || true)" + [ -n "$MATCH_VALUE" ] +} + +save_match_value() { + local value="$1" + [ -n "$value" ] || return 1 + mkdir -p "$(dirname "$STATE_PREFIX")" + printf '%s' "$value" > "$(state_file_for_mode)" +} + +launch_app() { + nohup ${launchTokens} >/dev/null 2>&1 & +} + +should_use_single_window() { + [ "$SINGLE_WINDOW" = "1" ] || [ "$FORCE_SINGLE_WINDOW" = "1" ] +} + +focus_niri() { + command -v niri >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + + local windows_json + windows_json="$(niri msg -j windows 2>/dev/null || niri msg --json windows 2>/dev/null)" || return 1 + + local win_id + win_id="$(printf '%s\n' "$windows_json" | jq -r --arg mode "$MATCH_MODE" --arg needle "$MATCH_VALUE" ' + def appid: (.app_id // ""); + def classv: (.class // .app_id // ""); + def titlev: (.title // .name // ""); + def anyv: ([appid, classv, titlev] | join(" ")); + def down($v): ($v | ascii_downcase); + def hit: + if $mode == "app-id" then down(appid) | contains(down($needle)) + elif $mode == "class" then down(classv) | contains(down($needle)) + elif $mode == "title" then down(titlev) | contains(down($needle)) + else down(anyv) | contains(down($needle)) + end; + map(select(hit)) | .[0].id // empty + ' | head -n1)" + + [ -n "$win_id" ] || return 1 + niri msg action focus-window --id "$win_id" >/dev/null 2>&1 +} + +capture_before_niri() { + command -v niri >/dev/null 2>&1 || { + echo "[]" + return 0 + } + command -v jq >/dev/null 2>&1 || { + echo "[]" + return 0 + } + + local windows_json + windows_json="$(niri msg -j windows 2>/dev/null || niri msg --json windows 2>/dev/null)" || { + echo "[]" + return 0 + } + + printf '%s\n' "$windows_json" | jq -c 'map((.id | tostring))' 2>/dev/null || echo "[]" +} + +wait_detect_niri() { + command -v niri >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + local before_ids="$1" + local attempts=40 + + while [ "$attempts" -gt 0 ]; do + local windows_json + windows_json="$(niri msg -j windows 2>/dev/null || niri msg --json windows 2>/dev/null)" || { + attempts=$((attempts - 1)) + sleep 0.2 + continue + } + + local detected + detected="$(printf '%s\n' "$windows_json" | jq -r --arg mode "$MATCH_MODE" --argjson before "$before_ids" ' + def appid: (.app_id // ""); + def classv: (.class // .app_id // ""); + def titlev: (.title // .name // ""); + def anyv: (([appid, classv, titlev] | map(select(length > 0))) | .[0] // ""); + def val: + if $mode == "app-id" then appid + elif $mode == "class" then classv + elif $mode == "title" then titlev + else anyv + end; + [ + .[] + | select(((.id | tostring) as $id | ($before | index($id) | not))) + | val + | select(length > 0) + ] + | .[0] // empty + ' | head -n1)" + + if [ -n "$detected" ]; then + printf '%s' "$detected" + return 0 + fi + + attempts=$((attempts - 1)) + sleep 0.2 + done + + return 1 +} + +focus_hyprland() { + command -v hyprctl >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + + local win_addr + win_addr="$(hyprctl -j clients 2>/dev/null | jq -r --arg mode "$MATCH_MODE" --arg needle "$MATCH_VALUE" ' + def appid: (.class // .initialClass // ""); + def classv: (.class // .initialClass // ""); + def titlev: (.title // ""); + def anyv: ([appid, classv, titlev] | join(" ")); + def down($v): ($v | ascii_downcase); + def hit: + if $mode == "app-id" then down(appid) | contains(down($needle)) + elif $mode == "class" then down(classv) | contains(down($needle)) + elif $mode == "title" then down(titlev) | contains(down($needle)) + else down(anyv) | contains(down($needle)) + end; + map(select(hit)) | .[0].address // empty + ' | head -n1)" + + [ -n "$win_addr" ] || return 1 + hyprctl dispatch focuswindow "address:$win_addr" >/dev/null 2>&1 +} + +capture_before_hyprland() { + command -v hyprctl >/dev/null 2>&1 || { + echo "[]" + return 0 + } + command -v jq >/dev/null 2>&1 || { + echo "[]" + return 0 + } + + hyprctl -j clients 2>/dev/null | jq -c 'map((.address // "") | tostring)' 2>/dev/null || echo "[]" +} + +wait_detect_hyprland() { + command -v hyprctl >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + local before_ids="$1" + local attempts=40 + + while [ "$attempts" -gt 0 ]; do + local detected + detected="$(hyprctl -j clients 2>/dev/null | jq -r --arg mode "$MATCH_MODE" --argjson before "$before_ids" ' + def appid: (.class // .initialClass // ""); + def classv: (.class // .initialClass // ""); + def titlev: (.title // ""); + def anyv: (([appid, classv, titlev] | map(select(length > 0))) | .[0] // ""); + def val: + if $mode == "app-id" then appid + elif $mode == "class" then classv + elif $mode == "title" then titlev + else anyv + end; + [ + .[] + | select((((.address // "") | tostring) as $id | ($before | index($id) | not))) + | val + | select(length > 0) + ] + | .[0] // empty + ' | head -n1)" + + if [ -n "$detected" ]; then + printf '%s' "$detected" + return 0 + fi + + attempts=$((attempts - 1)) + sleep 0.2 + done + + return 1 +} + +focus_i3_like() { + local wm_bin="$1" + command -v "$wm_bin" >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + + local con_id + con_id="$($wm_bin -t get_tree 2>/dev/null | jq -r --arg mode "$MATCH_MODE" --arg needle "$MATCH_VALUE" ' + def appid: (.app_id // .window_properties.class // .window_properties.instance // ""); + def classv: (.window_properties.class // .window_properties.instance // .app_id // ""); + def titlev: (.name // ""); + def anyv: ([appid, classv, titlev] | join(" ")); + def down($v): ($v | ascii_downcase); + def hit: + if $mode == "app-id" then down(appid) | contains(down($needle)) + elif $mode == "class" then down(classv) | contains(down($needle)) + elif $mode == "title" then down(titlev) | contains(down($needle)) + else down(anyv) | contains(down($needle)) + end; + [ + recurse(.nodes[]?, .floating_nodes[]?) + | select(has("id")) + | select(hit) + | .id + ] + | .[0] // empty + ' | head -n1)" + + [ -n "$con_id" ] || return 1 + "$wm_bin" "[con_id=$con_id] focus" >/dev/null 2>&1 +} + +capture_before_i3_like() { + local wm_bin="$1" + command -v "$wm_bin" >/dev/null 2>&1 || { + echo "[]" + return 0 + } + command -v jq >/dev/null 2>&1 || { + echo "[]" + return 0 + } + + "$wm_bin" -t get_tree 2>/dev/null | jq -c ' + [ + recurse(.nodes[]?, .floating_nodes[]?) + | select(has("id")) + | (.id | tostring) + ] + ' 2>/dev/null || echo "[]" +} + +wait_detect_i3_like() { + local wm_bin="$1" + command -v "$wm_bin" >/dev/null 2>&1 || return 1 + command -v jq >/dev/null 2>&1 || return 1 + local before_ids="$2" + local attempts=40 + + while [ "$attempts" -gt 0 ]; do + local detected + detected="$($wm_bin -t get_tree 2>/dev/null | jq -r --arg mode "$MATCH_MODE" --argjson before "$before_ids" ' + def appid: (.app_id // .window_properties.class // .window_properties.instance // ""); + def classv: (.window_properties.class // .window_properties.instance // .app_id // ""); + def titlev: (.name // ""); + def anyv: (([appid, classv, titlev] | map(select(length > 0))) | .[0] // ""); + def val: + if $mode == "app-id" then appid + elif $mode == "class" then classv + elif $mode == "title" then titlev + else anyv + end; + [ + recurse(.nodes[]?, .floating_nodes[]?) + | select(has("id")) + | select(((.id | tostring) as $id | ($before | index($id) | not))) + | val + | select(length > 0) + ] + | .[0] // empty + ' | head -n1)" + + if [ -n "$detected" ]; then + printf '%s' "$detected" + return 0 + fi + + attempts=$((attempts - 1)) + sleep 0.2 + done + + return 1 +} + +focus_custom() { + [ -n "$CUSTOM_FOCUS_COMMAND_TEMPLATE" ] || return 1 + [ -n "$MATCH_VALUE" ] || return 1 + local command_to_run="$CUSTOM_FOCUS_COMMAND_TEMPLATE" + command_to_run="\${command_to_run//\{match\}/$MATCH_VALUE}" + command_to_run="\${command_to_run//\{mode\}/$MATCH_MODE}" + sh -c "$command_to_run" >/dev/null 2>&1 +} + +try_focus_existing() { + should_use_single_window || return 1 + [ -n "$MATCH_VALUE" ] || return 1 + + case "$WINDOW_MANAGER" in + niri) + focus_niri + ;; + hyprland) + focus_hyprland + ;; + sway) + focus_i3_like swaymsg + ;; + i3) + focus_i3_like i3-msg + ;; + custom) + focus_custom + ;; + *) + return 1 + ;; + esac +} + +capture_before_ids() { + case "$WINDOW_MANAGER" in + niri) + capture_before_niri + ;; + hyprland) + capture_before_hyprland + ;; + sway) + capture_before_i3_like swaymsg + ;; + i3) + capture_before_i3_like i3-msg + ;; + *) + echo "[]" + ;; + esac +} + +wait_detect_value() { + local before_ids="$1" + case "$WINDOW_MANAGER" in + niri) + wait_detect_niri "$before_ids" + ;; + hyprland) + wait_detect_hyprland "$before_ids" + ;; + sway) + wait_detect_i3_like swaymsg "$before_ids" + ;; + i3) + wait_detect_i3_like i3-msg "$before_ids" + ;; + *) + return 1 + ;; + esac +} + +if should_use_single_window; then + MATCH_VALUE="" + if load_match_value; then + if try_focus_existing; then + exit 0 + fi + fi + + BEFORE_IDS="$(capture_before_ids)" + launch_app + + DETECTED_VALUE="$(wait_detect_value "$BEFORE_IDS" || true)" + if [ -n "$DETECTED_VALUE" ]; then + save_match_value "$DETECTED_VALUE" || true + fi + + exit 0 +fi + +launch_app +`; + + await fs.writeFile(options.launcherScriptPath, content, "utf8"); + await fs.chmod(options.launcherScriptPath, 0o755); +} + +function sanitizeDesktopValue(value: string): string { + return value.replace(/[\r\n]+/g, " ").trim(); +} + +function parseDesktopBoolean( + value: string | undefined, + fallback: boolean, +): boolean { + if (!value) { + return fallback; + } + if (value.toLowerCase() === "true") { + return true; + } + if (value.toLowerCase() === "false") { + return false; + } + return fallback; +} + +async function parseManagedEntry( + filePath: string, +): Promise { + const content = await fs.readFile(filePath, "utf8"); + const data = parseDesktopEntry(content); + if (data[MANAGED_MARKER_KEY] !== MANAGED_MARKER_VALUE) { + return undefined; + } + + const stats = await fs.stat(filePath).catch(() => undefined); + const name = data.Name?.trim(); + const url = data[MANAGED_URL_KEY]?.trim(); + if (!name || !url) { + return undefined; + } + + return { + id: data[MANAGED_ID_KEY]?.trim() || path.basename(filePath, ".desktop"), + name, + url, + comment: data.Comment?.trim() || undefined, + shortcut: data[MANAGED_SHORTCUT_KEY]?.trim() || undefined, + browserCommand: + data[MANAGED_COMMAND_KEY]?.trim() || DEFAULT_BROWSER_COMMAND, + browserArgsTemplate: + data[MANAGED_ARGS_KEY]?.trim() || DEFAULT_BROWSER_ARGS_TEMPLATE, + singleWindow: parseDesktopBoolean(data[MANAGED_SINGLE_WINDOW_KEY], false), + windowMatchMode: parseWindowMatchMode(data[MANAGED_MATCH_MODE_KEY]), + windowMatchValue: data[MANAGED_MATCH_VALUE_KEY]?.trim() || undefined, + windowManager: parseWindowManager(data[MANAGED_WM_KEY]), + customFocusCommandTemplate: + data[MANAGED_CUSTOM_FOCUS_KEY]?.trim() || undefined, + desktopFilePath: filePath, + desktopFileName: path.basename(filePath), + launcherScriptPath: data[MANAGED_LAUNCHER_KEY]?.trim() || undefined, + icon: data.Icon?.trim() || undefined, + updatedAt: stats?.mtime, + }; +} + +function parseDesktopEntry(content: string): Record { + const result: Record = {}; + let inDesktopEntrySection = false; + + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) { + continue; + } + + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + inDesktopEntrySection = trimmed === "[Desktop Entry]"; + continue; + } + + if (!inDesktopEntrySection) { + continue; + } + + const separatorIndex = line.indexOf("="); + if (separatorIndex <= 0) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + if (key) { + result[key] = value; + } + } + + return result; +} + +async function downloadFavicon( + url: string, + id: string, + iconDirectory: string, +): Promise { + const pageUrl = new URL(url); + const faviconUrl = getDuckDuckGoFaviconUrl(pageUrl.hostname); + if (!faviconUrl) { + return undefined; + } + + try { + const response = await fetchWithTimeout(faviconUrl, 7000); + if (!response.ok) { + return undefined; + } + + const contentType = response.headers.get("content-type") || ""; + if (!contentType.startsWith("image/")) { + return undefined; + } + + const iconBytes = Buffer.from(await response.arrayBuffer()); + if (iconBytes.length === 0) { + return undefined; + } + + const preparedIcon = await prepareDownloadedIcon( + iconBytes, + contentType, + faviconUrl, + ); + await removeOldIcons(id, iconDirectory); + const outputPath = path.join( + iconDirectory, + `${id}.${preparedIcon.extension}`, + ); + await fs.writeFile(outputPath, preparedIcon.bytes); + return outputPath; + } catch { + // best effort, keep the existing icon or fall back to the browser icon + } + + return undefined; +} + +function getDuckDuckGoFaviconUrl(domain: string): string | undefined { + const normalizedDomain = domain.trim(); + if (!normalizedDomain) { + return undefined; + } + + return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(normalizedDomain)}.ico`; +} + +async function prepareDownloadedIcon( + iconBytes: Buffer, + contentType: string, + faviconUrl: string, +): Promise<{ bytes: Buffer; extension: string }> { + const extension = detectIconExtension(contentType, faviconUrl); + if (extension !== "webp") { + return { bytes: iconBytes, extension }; + } + + const convertedBytes = convertWebpToPng(iconBytes); + return convertedBytes + ? { bytes: convertedBytes, extension: "png" } + : { bytes: iconBytes, extension }; +} + +function convertWebpToPng(iconBytes: Buffer): Buffer | undefined { + try { + const decodedIcon = decodeWebp(iconBytes); + return Buffer.from( + encodePng({ + width: decodedIcon.width, + height: decodedIcon.height, + data: decodedIcon.data, + channels: 4, + depth: 8, + }), + ); + } catch { + // keep the original WebP when conversion fails + } + + return undefined; +} + +async function removeOldIcons( + id: string, + iconDirectory: string, +): Promise { + await fs.mkdir(iconDirectory, { recursive: true }); + const files = await fs.readdir(iconDirectory).catch(() => [] as string[]); + await Promise.all( + files + .filter((file) => file.startsWith(`${id}.`)) + .map((file) => + fs.unlink(path.join(iconDirectory, file)).catch(() => undefined), + ), + ); +} + +async function removeStateFiles( + id: string, + stateDirectory: string, +): Promise { + await fs.mkdir(stateDirectory, { recursive: true }); + const files = await fs.readdir(stateDirectory).catch(() => [] as string[]); + await Promise.all( + files + .filter((file) => file === id || file.startsWith(`${id}.`)) + .map((file) => + fs.unlink(path.join(stateDirectory, file)).catch(() => undefined), + ), + ); +} + +function detectIconExtension( + contentType: string, + candidateUrl: string, +): string { + const normalizedContentType = contentType.split(";")[0].trim().toLowerCase(); + const byType: Record = { + "image/png": "png", + "image/jpeg": "jpg", + "image/jpg": "jpg", + "image/gif": "gif", + "image/svg+xml": "svg", + "image/webp": "webp", + "image/x-icon": "ico", + "image/vnd.microsoft.icon": "ico", + }; + if (byType[normalizedContentType]) { + return byType[normalizedContentType]; + } + + try { + const parsed = new URL(candidateUrl); + const extension = path + .extname(parsed.pathname) + .toLowerCase() + .replace(".", ""); + if (extension) { + return extension; + } + } catch { + // ignore + } + + return "ico"; +} + +async function fetchWithTimeout( + url: string, + timeoutMs: number, + init?: RequestInit, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { + ...init, + signal: controller.signal, + redirect: "follow", + }); + } finally { + clearTimeout(timeout); + } +} + +export function hostnameFromUrl(url: string): string { + try { + return new URL(url).hostname; + } catch { + return ""; + } +} diff --git a/extensions/webapp-manager/src/manage-desktop-entries.tsx b/extensions/webapp-manager/src/manage-desktop-entries.tsx new file mode 100644 index 00000000..5d50a0b8 --- /dev/null +++ b/extensions/webapp-manager/src/manage-desktop-entries.tsx @@ -0,0 +1,1717 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Action, + ActionPanel, + Alert, + Clipboard, + confirmAlert, + environment, + Form, + getPreferenceValues, + Icon, + List, + showToast, + Toast, + useNavigation, +} from "@vicinae/api"; +import { spawn } from "node:child_process"; +import * as fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { + DEFAULT_BROWSER_ARGS_TEMPLATE, + DEFAULT_BROWSER_COMMAND, + DEFAULT_DESKTOP_DIRECTORY, + DEFAULT_WINDOW_MANAGER, + DEFAULT_WINDOW_MATCH_MODE, + deleteManagedEntry, + type EntryDraft, + hostnameFromUrl, + launchScriptPathForEntry, + listManagedEntries, + parseWindowManager, + parseWindowMatchMode, + resolvePath, + saveManagedEntry, + type ManagedDesktopEntry, + type WindowManager, +} from "./lib/desktop-entry-manager"; + +type ExtensionPreferences = { + desktopEntryDirectory?: string; + browserCommand?: string; + browserArgsTemplate?: string; + windowManager?: WindowManager | string; + customWindowFocusCommand?: string; +}; + +type EntryFormProps = { + mode: "create" | "edit"; + entry?: ManagedDesktopEntry; + defaultUrl?: string; + defaultBrowserCommand: string; + defaultBrowserArgsTemplate: string; + onSubmitEntry: ( + draft: EntryDraft, + existing?: ManagedDesktopEntry, + ) => Promise; +}; + +type ImportConfigFormProps = { + entries: ManagedDesktopEntry[]; + defaultBrowserCommand: string; + defaultBrowserArgsTemplate: string; + onImportEntries: (entries: WebappConfigEntry[]) => Promise; +}; + +type WebappConfigEntry = { + id?: string; + name: string; + url: string; + comment?: string; + shortcut?: string; + browserCommand: string; + browserArgsTemplate: string; + singleWindow: boolean; + windowMatchMode: string; +}; + +type WebappConfigExport = { + schema: "vicinae-webapp-config"; + version: 1; + exportedAt: string; + entries: WebappConfigEntry[]; +}; + +type ShortcutInstallTarget = { + windowManager: WindowManager; + mainConfigPath: string; + shortcutConfigPath: string; + includeLine: string; + reloadCommand?: string[]; +}; + +type GlobalShortcut = { + modifiers: string[]; + key: string; + normalized: string; +}; + +type InstallGlobalShortcutConfigOptions = { + entries: ManagedDesktopEntry[]; + directory: string; + iconDirectory: string; + launcherDirectory: string; + stateDirectory: string; + windowManager: WindowManager; + customFocusCommandTemplate?: string; +}; + +export default function ManageDesktopEntries() { + const preferences = getPreferenceValues(); + const desktopEntryDirectory = resolvePath( + preferences.desktopEntryDirectory || DEFAULT_DESKTOP_DIRECTORY, + ); + const defaultBrowserCommand = + preferences.browserCommand?.trim() || DEFAULT_BROWSER_COMMAND; + const defaultBrowserArgsTemplate = + preferences.browserArgsTemplate?.trim() || DEFAULT_BROWSER_ARGS_TEMPLATE; + const windowManager = parseWindowManager( + preferences.windowManager || DEFAULT_WINDOW_MANAGER, + ); + const customWindowFocusCommand = + preferences.customWindowFocusCommand?.trim() || undefined; + + const iconDirectory = useMemo( + () => path.join(environment.supportPath, "desktop-entry-favicons"), + [], + ); + const launcherDirectory = useMemo( + () => path.join(environment.supportPath, "desktop-entry-launchers"), + [], + ); + const stateDirectory = useMemo( + () => path.join(environment.supportPath, "desktop-entry-window-state"), + [], + ); + const exportFilePath = useMemo( + () => path.join(environment.supportPath, "webapp-config-export.json"), + [], + ); + + const [entries, setEntries] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchText, setSearchText] = useState(""); + + const reloadEntries = useCallback(async () => { + setIsLoading(true); + try { + setEntries(await listManagedEntries(desktopEntryDirectory)); + } catch (error) { + await showToast({ + style: Toast.Style.Failure, + title: "Could not load desktop entries", + message: getErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + }, [desktopEntryDirectory]); + + useEffect(() => { + void reloadEntries(); + }, [reloadEntries]); + + const upsertEntry = useCallback( + async (draft: EntryDraft, existing?: ManagedDesktopEntry) => { + const operation = existing ? "Updating" : "Creating"; + const toast = await showToast({ + style: Toast.Style.Animated, + title: `${operation} desktop entry...`, + }); + + try { + const savedEntry = await saveManagedEntry({ + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + draft, + existingEntry: existing, + }); + const latestEntries = await listManagedEntries(desktopEntryDirectory); + setEntries(latestEntries); + + let shortcutError: string | undefined; + try { + await installGlobalShortcutConfig({ + entries: latestEntries, + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + }); + } catch (error) { + shortcutError = getErrorMessage(error); + } + + toast.style = shortcutError ? Toast.Style.Failure : Toast.Style.Success; + toast.title = shortcutError + ? "Desktop entry saved, shortcuts failed" + : existing + ? "Desktop entry updated" + : "Desktop entry created"; + toast.message = shortcutError || savedEntry.desktopFileName; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = existing + ? "Failed to update desktop entry" + : "Failed to create desktop entry"; + toast.message = getErrorMessage(error); + throw error; + } + }, + [ + customWindowFocusCommand, + desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + ], + ); + + const handleDelete = useCallback( + async (entry: ManagedDesktopEntry) => { + const confirmed = await confirmAlert({ + title: `Delete \"${entry.name}\"?`, + message: `This removes ${entry.desktopFileName}.`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }); + if (!confirmed) { + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Deleting desktop entry...", + }); + try { + await deleteManagedEntry( + entry, + iconDirectory, + launcherDirectory, + stateDirectory, + ); + const latestEntries = await listManagedEntries(desktopEntryDirectory); + setEntries(latestEntries); + + let shortcutError: string | undefined; + try { + await installGlobalShortcutConfig({ + entries: latestEntries, + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + }); + } catch (error) { + shortcutError = getErrorMessage(error); + } + + toast.style = shortcutError ? Toast.Style.Failure : Toast.Style.Success; + toast.title = shortcutError + ? "Desktop entry deleted, shortcuts failed" + : "Desktop entry deleted"; + toast.message = shortcutError || entry.desktopFileName; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to delete desktop entry"; + toast.message = getErrorMessage(error); + } + }, + [ + customWindowFocusCommand, + desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + ], + ); + + const handleRefreshFavicon = useCallback( + async (entry: ManagedDesktopEntry) => { + await upsertEntry(entryToDraft(entry, true), entry); + }, + [upsertEntry], + ); + + const handleRefreshAllFavicons = useCallback(async () => { + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Refreshing favicons...", + message: `${entries.length} webapps`, + }); + let refreshed = 0; + let failed = 0; + let shortcutCount = 0; + let shortcutError: string | undefined; + + try { + for (const entry of entries) { + try { + await saveManagedEntry({ + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + draft: entryToDraft(entry, true), + existingEntry: entry, + }); + refreshed += 1; + } catch { + failed += 1; + } + } + + const latestEntries = await listManagedEntries(desktopEntryDirectory); + setEntries(latestEntries); + + try { + shortcutCount = await installGlobalShortcutConfig({ + entries: latestEntries, + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + }); + } catch (error) { + shortcutError = getErrorMessage(error); + } + + toast.style = + failed > 0 || shortcutError ? Toast.Style.Failure : Toast.Style.Success; + toast.title = + failed > 0 || shortcutError + ? "Some updates failed" + : "Favicons and shortcuts refreshed"; + toast.message = shortcutError + ? `${refreshed} favicons refreshed, ${failed} failed. Shortcuts: ${shortcutError}` + : `${refreshed} favicons refreshed, ${failed} failed, ${shortcutCount} shortcuts installed`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to refresh favicons"; + toast.message = getErrorMessage(error); + } + }, [ + customWindowFocusCommand, + desktopEntryDirectory, + entries, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + ]); + + const handleExportConfig = useCallback(async () => { + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Exporting webapp config...", + }); + + try { + const exportJson = JSON.stringify(buildConfigExport(entries), null, 2); + await fs.mkdir(environment.supportPath, { recursive: true }); + await fs.writeFile(exportFilePath, `${exportJson}\n`, "utf8"); + await Clipboard.copy(exportJson); + toast.style = Toast.Style.Success; + toast.title = "Webapp config exported"; + toast.message = `Copied JSON and wrote ${path.basename(exportFilePath)}`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to export webapp config"; + toast.message = getErrorMessage(error); + } + }, [entries, exportFilePath]); + + const handleImportEntries = useCallback( + async (configEntries: WebappConfigEntry[]) => { + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Importing webapp config...", + }); + let created = 0; + let updated = 0; + + try { + for (const configEntry of configEntries) { + const existing = findMatchingImportEntry(configEntry, entries); + await saveManagedEntry({ + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + draft: configEntryToDraft(configEntry), + existingEntry: existing, + }); + if (existing) { + updated += 1; + } else { + created += 1; + } + } + + await reloadEntries(); + toast.style = Toast.Style.Success; + toast.title = "Webapp config imported"; + toast.message = `${created} created, ${updated} updated`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to import webapp config"; + toast.message = getErrorMessage(error); + throw error; + } + }, + [ + customWindowFocusCommand, + desktopEntryDirectory, + entries, + iconDirectory, + launcherDirectory, + reloadEntries, + stateDirectory, + windowManager, + ], + ); + + const handleInstallGlobalShortcuts = useCallback(async () => { + const entriesWithShortcuts = entries.filter((entry) => + Boolean(entry.shortcut?.trim()), + ); + if (entriesWithShortcuts.length === 0) { + await showToast({ + style: Toast.Style.Failure, + title: "No global shortcuts configured", + }); + return; + } + + const installTarget = getShortcutInstallTarget(windowManager); + if (!installTarget) { + await showToast({ + style: Toast.Style.Failure, + title: "Global shortcuts are not supported", + message: "Choose niri, Hyprland, Sway, or i3 in preferences.", + }); + return; + } + + const confirmed = await confirmAlert({ + title: "Install global shortcuts?", + message: `This writes ${installTarget.shortcutConfigPath} and updates ${installTarget.mainConfigPath} if needed.`, + primaryAction: { + title: "Install", + }, + }); + if (!confirmed) { + return; + } + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Installing global shortcuts...", + }); + + try { + const shortcutCount = await installGlobalShortcutConfig({ + entries, + directory: desktopEntryDirectory, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + customFocusCommandTemplate: customWindowFocusCommand, + }); + + toast.style = Toast.Style.Success; + toast.title = "Global shortcuts installed"; + toast.message = `${shortcutCount} shortcuts`; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to install global shortcuts"; + toast.message = getErrorMessage(error); + } + }, [ + customWindowFocusCommand, + desktopEntryDirectory, + entries, + iconDirectory, + launcherDirectory, + stateDirectory, + windowManager, + ]); + + const createEntryTarget = useCallback( + (defaultUrl?: string) => ( + + ), + [defaultBrowserArgsTemplate, defaultBrowserCommand, upsertEntry], + ); + + return ( + + + void handleExportConfig()} + /> + void handleInstallGlobalShortcuts()} + /> + void handleRefreshAllFavicons()} + /> + + } + /> + + } + > + {entries.length === 0 ? ( + + + + } + /> + + } + /> + ) : ( + entries.map((entry) => ( + + + } + /> + + void handleExportConfig()} + /> + void handleRefreshAllFavicons()} + /> + + } + /> + + void launchEntry(entry, launcherDirectory)} + /> + void handleInstallGlobalShortcuts()} + /> + void handleRefreshFavicon(entry)} + /> + + + + void handleDelete(entry)} + /> + + } + /> + )) + )} + + ); +} + +function EntryForm(props: EntryFormProps) { + const { pop } = useNavigation(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const existing = props.entry; + const formTitle = + props.mode === "create" ? "Create Desktop Entry" : "Edit Desktop Entry"; + const [singleWindow, setSingleWindow] = useState( + existing?.singleWindow || false, + ); + const [windowMatchMode, setWindowMatchMode] = useState( + existing?.windowMatchMode || DEFAULT_WINDOW_MATCH_MODE, + ); + + useEffect(() => { + setSingleWindow(existing?.singleWindow || false); + setWindowMatchMode(existing?.windowMatchMode || DEFAULT_WINDOW_MATCH_MODE); + }, [existing?.id, existing?.singleWindow, existing?.windowMatchMode]); + + const handleSubmit = useCallback( + async (values: Form.Values) => { + if (isSubmitting) { + return; + } + + const draft: EntryDraft = { + name: getStringValue(values, "name"), + url: getStringValue(values, "url"), + comment: getStringValue(values, "comment"), + shortcut: normalizeShortcut(getStringValue(values, "shortcut")), + browserCommand: getStringValue(values, "browserCommand"), + browserArgsTemplate: getStringValue(values, "browserArgsTemplate"), + singleWindow, + windowMatchMode: parseWindowMatchMode(windowMatchMode), + downloadFavicon: true, + }; + + if (!draft.name.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "Name is required", + }); + return; + } + if (!draft.url.trim()) { + await showToast({ + style: Toast.Style.Failure, + title: "URL is required", + }); + return; + } + if (draft.shortcut && !isValidGlobalShortcut(draft.shortcut)) { + await showToast({ + style: Toast.Style.Failure, + title: "Shortcut is invalid", + message: "Use values like ctrl+shift+g or alt+space.", + }); + return; + } + + setIsSubmitting(true); + try { + await props.onSubmitEntry(draft, existing); + pop(); + } catch { + // error toast is shown by the parent handler + } finally { + setIsSubmitting(false); + } + }, + [existing, isSubmitting, pop, props, singleWindow, windowMatchMode], + ); + + return ( +
+ + + } + > + + + + + + + + + + + setWindowMatchMode(parseWindowMatchMode(newValue)) + } + info="Launcher will auto-detect and remember the match value after first launch." + > + + + + + + + ); +} + +function ImportConfigForm(props: ImportConfigFormProps) { + const { pop } = useNavigation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [jsonText, setJsonText] = useState(""); + + const handlePasteFromClipboard = useCallback(async () => { + const text = await Clipboard.readText(); + setJsonText(text); + }, []); + + const handleSubmit = useCallback( + async (values: Form.Values) => { + if (isSubmitting) { + return; + } + + setIsSubmitting(true); + try { + const importText = await readImportText(values, jsonText); + const importedEntries = parseWebappConfigExport( + importText, + props.defaultBrowserCommand, + props.defaultBrowserArgsTemplate, + ); + await props.onImportEntries(importedEntries); + pop(); + } catch (error) { + await showToast({ + style: Toast.Style.Failure, + title: "Import failed", + message: getErrorMessage(error), + }); + } finally { + setIsSubmitting(false); + } + }, + [ + isSubmitting, + jsonText, + pop, + props.defaultBrowserArgsTemplate, + props.defaultBrowserCommand, + props.onImportEntries, + ], + ); + + return ( +
+ + void handlePasteFromClipboard()} + /> + + } + > + + + + + ); +} + +function getStringValue(values: Form.Values, key: string): string { + const value = values[key]; + return typeof value === "string" ? value : ""; +} + +function getStringArrayValue(values: Form.Values, key: string): string[] { + const value = values[key]; + return Array.isArray(value) && value.every((item) => typeof item === "string") + ? value + : []; +} + +async function readImportText( + values: Form.Values, + fallbackJsonText: string, +): Promise { + const configFiles = getStringArrayValue(values, "configFile"); + if (configFiles[0]) { + return fs.readFile(configFiles[0], "utf8"); + } + + const formJsonText = getStringValue(values, "configJson"); + const importText = formJsonText.trim() || fallbackJsonText.trim(); + if (!importText) { + throw new Error("Choose a JSON file or paste exported JSON."); + } + return importText; +} + +function buildConfigExport(entries: ManagedDesktopEntry[]): WebappConfigExport { + return { + schema: "vicinae-webapp-config", + version: 1, + exportedAt: new Date().toISOString(), + entries: entries.map((entry) => ({ + id: entry.id, + name: entry.name, + url: entry.url, + comment: entry.comment, + shortcut: entry.shortcut, + browserCommand: entry.browserCommand, + browserArgsTemplate: entry.browserArgsTemplate, + singleWindow: entry.singleWindow, + windowMatchMode: entry.windowMatchMode, + })), + }; +} + +function parseWebappConfigExport( + jsonText: string, + defaultBrowserCommand: string, + defaultBrowserArgsTemplate: string, +): WebappConfigEntry[] { + let parsed: unknown; + try { + parsed = JSON.parse(jsonText); + } catch { + throw new Error("JSON is invalid."); + } + + const rawEntries = Array.isArray(parsed) + ? parsed + : isRecord(parsed) && Array.isArray(parsed.entries) + ? parsed.entries + : undefined; + if (!rawEntries) { + throw new Error("JSON must contain an entries array."); + } + + const entries = rawEntries.map((entry, index) => + parseWebappConfigEntry( + entry, + index, + defaultBrowserCommand, + defaultBrowserArgsTemplate, + ), + ); + if (entries.length === 0) { + throw new Error("Config does not contain any webapp entries."); + } + + return entries; +} + +function parseWebappConfigEntry( + value: unknown, + index: number, + defaultBrowserCommand: string, + defaultBrowserArgsTemplate: string, +): WebappConfigEntry { + if (!isRecord(value)) { + throw new Error(`Entry ${index + 1} must be an object.`); + } + + const name = optionalString(value.name)?.trim(); + const url = optionalString(value.url)?.trim(); + if (!name) { + throw new Error(`Entry ${index + 1} is missing a name.`); + } + if (!url) { + throw new Error(`Entry ${index + 1} is missing a URL.`); + } + + const shortcut = normalizeShortcut(optionalString(value.shortcut) || ""); + if (shortcut && !isValidGlobalShortcut(shortcut)) { + throw new Error(`Entry ${index + 1} has an invalid shortcut.`); + } + + return { + id: optionalString(value.id)?.trim() || undefined, + name, + url, + comment: optionalString(value.comment)?.trim() || undefined, + shortcut, + browserCommand: + optionalString(value.browserCommand)?.trim() || defaultBrowserCommand, + browserArgsTemplate: + optionalString(value.browserArgsTemplate)?.trim() || + defaultBrowserArgsTemplate, + singleWindow: + typeof value.singleWindow === "boolean" ? value.singleWindow : false, + windowMatchMode: parseWindowMatchMode(value.windowMatchMode), + }; +} + +function configEntryToDraft(entry: WebappConfigEntry): EntryDraft { + return { + name: entry.name, + url: entry.url, + comment: entry.comment, + shortcut: entry.shortcut, + browserCommand: entry.browserCommand, + browserArgsTemplate: entry.browserArgsTemplate, + singleWindow: entry.singleWindow, + windowMatchMode: parseWindowMatchMode(entry.windowMatchMode), + downloadFavicon: true, + }; +} + +function entryToDraft( + entry: ManagedDesktopEntry, + downloadFavicon: boolean, +): EntryDraft { + return { + name: entry.name, + url: entry.url, + comment: entry.comment, + shortcut: entry.shortcut, + browserCommand: entry.browserCommand, + browserArgsTemplate: entry.browserArgsTemplate, + singleWindow: entry.singleWindow, + windowMatchMode: entry.windowMatchMode, + downloadFavicon, + }; +} + +function findMatchingImportEntry( + importedEntry: WebappConfigEntry, + entries: ManagedDesktopEntry[], +): ManagedDesktopEntry | undefined { + if (importedEntry.id) { + const idMatch = entries.find((entry) => entry.id === importedEntry.id); + if (idMatch) { + return idMatch; + } + } + + const importedComparableUrl = comparableUrl(importedEntry.url); + return entries.find( + (entry) => + entry.name === importedEntry.name && + comparableUrl(entry.url) === importedComparableUrl, + ); +} + +function comparableUrl(url: string): string { + try { + return new URL(url).toString(); + } catch { + return url.trim(); + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function optionalString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function buildAccessories(entry: ManagedDesktopEntry): List.Item.Accessory[] { + const accessories: List.Item.Accessory[] = []; + if (entry.shortcut) { + accessories.push({ icon: Icon.Keyboard, text: entry.shortcut }); + } + if (entry.singleWindow) { + accessories.push({ tag: `single:${entry.windowMatchMode}` }); + } + accessories.push({ text: hostnameFromUrl(entry.url) || "url" }); + if (entry.updatedAt) { + accessories.push({ text: entry.updatedAt }); + } else { + accessories.push({ text: entry.desktopFileName }); + } + return accessories; +} + +function entryToIcon(entry: ManagedDesktopEntry): URL | string | Icon { + if (entry.icon) { + if (/^https?:\/\//i.test(entry.icon)) { + try { + return new URL(entry.icon); + } catch { + return Icon.Globe; + } + } + + if ( + path.isAbsolute(entry.icon) || + entry.icon.startsWith("./") || + entry.icon.startsWith("../") + ) { + return entry.icon; + } + + return Icon.Globe; + } + + try { + return new URL("/favicon.ico", entry.url); + } catch { + return Icon.Globe; + } +} + +function guessUrlFromSearch(searchText: string): string | undefined { + const trimmed = searchText.trim(); + if (!trimmed) { + return undefined; + } + + if (trimmed.includes(" ")) { + return undefined; + } + + if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(trimmed)) { + return trimmed; + } + + if (trimmed.includes(".")) { + return `https://${trimmed}`; + } + + return undefined; +} + +async function launchEntry( + entry: ManagedDesktopEntry, + launcherDirectory: string, +): Promise { + const launcherScriptPath = launchScriptPathForEntry(entry, launcherDirectory); + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Opening webapp...", + message: entry.name, + }); + + try { + await new Promise((resolve, reject) => { + const child = spawn(launcherScriptPath, { + detached: true, + stdio: "ignore", + }); + child.once("error", reject); + child.once("spawn", () => { + child.unref(); + resolve(); + }); + }); + toast.style = Toast.Style.Success; + toast.title = "Webapp opened"; + toast.message = entry.name; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to open webapp"; + toast.message = getErrorMessage(error); + } +} + +async function installGlobalShortcutConfig( + options: InstallGlobalShortcutConfigOptions, +): Promise { + const entriesWithShortcuts = options.entries.filter((entry) => + Boolean(entry.shortcut?.trim()), + ); + + const installTarget = getShortcutInstallTarget(options.windowManager); + if (!installTarget) { + if (entriesWithShortcuts.length === 0) { + return 0; + } + throw new Error("Choose niri, Hyprland, Sway, or i3 in preferences."); + } + + for (const entry of entriesWithShortcuts) { + await saveManagedEntry({ + directory: options.directory, + iconDirectory: options.iconDirectory, + launcherDirectory: options.launcherDirectory, + stateDirectory: options.stateDirectory, + windowManager: options.windowManager, + customFocusCommandTemplate: options.customFocusCommandTemplate, + draft: entryToDraft(entry, false), + existingEntry: entry, + }); + } + + const configText = buildShortcutConfig( + entriesWithShortcuts, + installTarget.windowManager, + options.launcherDirectory, + ); + await fs.mkdir(path.dirname(installTarget.shortcutConfigPath), { + recursive: true, + }); + await fs.writeFile(installTarget.shortcutConfigPath, configText, "utf8"); + await ensureConfigIncludes(installTarget); + if (installTarget.reloadCommand) { + await runBestEffortCommand(installTarget.reloadCommand); + } + + return entriesWithShortcuts.length; +} + +function getShortcutInstallTarget( + windowManager: WindowManager, +): ShortcutInstallTarget | undefined { + const configHome = + process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); + + switch (windowManager) { + case "niri": { + const mainConfigPath = path.join(configHome, "niri", "config.kdl"); + return { + windowManager, + mainConfigPath, + shortcutConfigPath: path.join( + configHome, + "niri", + "vicinae-webapps.kdl", + ), + includeLine: 'include "vicinae-webapps.kdl"', + }; + } + case "hyprland": { + const shortcutConfigPath = path.join( + configHome, + "hypr", + "vicinae-webapps.conf", + ); + return { + windowManager, + mainConfigPath: path.join(configHome, "hypr", "hyprland.conf"), + shortcutConfigPath, + includeLine: `source = ${shortcutConfigPath}`, + reloadCommand: ["hyprctl", "reload"], + }; + } + case "sway": { + const shortcutConfigPath = path.join( + configHome, + "sway", + "vicinae-webapps.conf", + ); + return { + windowManager, + mainConfigPath: path.join(configHome, "sway", "config"), + shortcutConfigPath, + includeLine: `include ${shortcutConfigPath}`, + reloadCommand: ["swaymsg", "reload"], + }; + } + case "i3": { + const shortcutConfigPath = path.join( + configHome, + "i3", + "vicinae-webapps.conf", + ); + return { + windowManager, + mainConfigPath: path.join(configHome, "i3", "config"), + shortcutConfigPath, + includeLine: `include ${shortcutConfigPath}`, + reloadCommand: ["i3-msg", "reload"], + }; + } + case "custom": + return undefined; + } +} + +function buildShortcutConfig( + entries: ManagedDesktopEntry[], + windowManager: WindowManager, + launcherDirectory: string, +): string { + const generatedAt = new Date().toISOString(); + const lines = [ + commentForWindowManager( + windowManager, + "Generated by Vicinae Manage Webapps.", + ), + commentForWindowManager( + windowManager, + "Re-run Install Global Shortcuts after changing webapps.", + ), + commentForWindowManager(windowManager, `Generated at ${generatedAt}.`), + "", + ]; + const seenShortcuts = new Map(); + const bindings = entries.map((entry) => { + const shortcut = parseGlobalShortcut(entry.shortcut); + const duplicateEntryName = seenShortcuts.get(shortcut.normalized); + if (duplicateEntryName) { + throw new Error( + `Duplicate shortcut ${entry.shortcut} for ${duplicateEntryName} and ${entry.name}.`, + ); + } + seenShortcuts.set(shortcut.normalized, entry.name); + + return buildShortcutBinding( + entry, + shortcut, + windowManager, + launchScriptPathForEntry(entry, launcherDirectory), + ); + }); + + if (windowManager === "niri") { + return `${lines.join("\n")}binds {\n${bindings.map((line) => ` ${line}`).join("\n")}\n}\n`; + } + + return `${lines.join("\n")}${bindings.join("\n")}\n`; +} + +function buildShortcutBinding( + entry: ManagedDesktopEntry, + shortcut: GlobalShortcut, + windowManager: WindowManager, + commandPath: string, +): string { + switch (windowManager) { + case "niri": + return `${formatShortcutForNiri(shortcut)} hotkey-overlay-title=${quoteKdlString(`Open ${entry.name}`)} { spawn "env" "VICINAE_FORCE_SINGLE_WINDOW=1" ${quoteKdlString(commandPath)}; }`; + case "hyprland": + return `bind = ${formatModifiersForHyprland(shortcut.modifiers)}, ${formatKeyForHyprland(shortcut.key)}, exec, env VICINAE_FORCE_SINGLE_WINDOW=1 ${shellQuote(commandPath)}`; + case "sway": + return `bindsym ${formatShortcutForI3Sway(shortcut)} exec env VICINAE_FORCE_SINGLE_WINDOW=1 ${shellQuote(commandPath)}`; + case "i3": + return `bindsym ${formatShortcutForI3Sway(shortcut)} exec --no-startup-id env VICINAE_FORCE_SINGLE_WINDOW=1 ${shellQuote(commandPath)}`; + case "custom": + throw new Error( + "Global shortcut install is not supported for custom window manager.", + ); + } +} + +function commentForWindowManager( + windowManager: WindowManager, + text: string, +): string { + return windowManager === "niri" ? `// ${text}` : `# ${text}`; +} + +async function ensureConfigIncludes( + installTarget: ShortcutInstallTarget, +): Promise { + let mainConfig: string; + try { + mainConfig = await fs.readFile(installTarget.mainConfigPath, "utf8"); + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") { + throw new Error( + `Window manager config not found: ${installTarget.mainConfigPath}`, + ); + } + throw error; + } + + if (mainConfig.includes(installTarget.includeLine)) { + return; + } + + const separator = mainConfig.endsWith("\n") ? "" : "\n"; + await fs.writeFile( + installTarget.mainConfigPath, + `${mainConfig}${separator}\n${commentForWindowManager(installTarget.windowManager, "Vicinae webapp global shortcuts")}\n${installTarget.includeLine}\n`, + "utf8", + ); +} + +async function runBestEffortCommand(command: string[]): Promise { + await new Promise((resolve) => { + const child = spawn(command[0], command.slice(1), { + detached: true, + stdio: "ignore", + }); + child.once("error", () => resolve()); + child.once("spawn", () => { + child.unref(); + resolve(); + }); + }); +} + +function parseGlobalShortcut(value: string | undefined): GlobalShortcut { + if (!value) { + throw new Error("Shortcut is empty."); + } + + const parts = value + .split("+") + .map((part) => part.trim()) + .filter(Boolean); + if (parts.length < 2) { + throw new Error("Shortcut must contain at least one modifier and one key."); + } + + const modifiers = parts.slice(0, -1).map((modifierPart) => { + const modifier = SHORTCUT_MODIFIER_ALIASES[modifierPart.toLowerCase()]; + if (!modifier) { + throw new Error(`Unsupported shortcut modifier: ${modifierPart}`); + } + return modifier; + }); + const uniqueModifiers = Array.from(new Set(modifiers)); + if (uniqueModifiers.length !== modifiers.length) { + throw new Error("Shortcut contains a duplicate modifier."); + } + + const keyPart = parts[parts.length - 1]; + const key = + SHORTCUT_KEY_ALIASES[keyPart.toLowerCase()] || keyPart.toLowerCase(); + if (!SHORTCUT_KEYS.has(key)) { + throw new Error(`Unsupported shortcut key: ${keyPart}`); + } + + return { + modifiers: uniqueModifiers, + key, + normalized: `${[...uniqueModifiers].sort().join("+")}+${key}`, + }; +} + +function isValidGlobalShortcut(value: string): boolean { + try { + parseGlobalShortcut(value); + return true; + } catch { + return false; + } +} + +function formatShortcutForNiri(shortcut: GlobalShortcut): string { + return [ + ...shortcut.modifiers.map(formatModifierForNiri), + formatKeyForNiri(shortcut.key), + ].join("+"); +} + +function formatModifierForNiri(modifier: string): string { + switch (modifier) { + case "cmd": + return "Super"; + case "ctrl": + return "Ctrl"; + case "opt": + return "Alt"; + case "shift": + return "Shift"; + default: + return modifier; + } +} + +function formatModifiersForHyprland(modifiers: string[]): string { + return modifiers.map(formatModifierForHyprland).join(" "); +} + +function formatModifierForHyprland(modifier: string): string { + switch (modifier) { + case "cmd": + return "SUPER"; + case "ctrl": + return "CTRL"; + case "opt": + return "ALT"; + case "shift": + return "SHIFT"; + default: + return modifier.toUpperCase(); + } +} + +function formatShortcutForI3Sway(shortcut: GlobalShortcut): string { + return [ + ...shortcut.modifiers.map(formatModifierForI3Sway), + formatKeyForI3Sway(shortcut.key), + ].join("+"); +} + +function formatModifierForI3Sway(modifier: string): string { + switch (modifier) { + case "cmd": + return "Mod4"; + case "ctrl": + return "Ctrl"; + case "opt": + return "Mod1"; + case "shift": + return "Shift"; + default: + return modifier; + } +} + +function formatKeyForNiri(key: string): string { + if (/^[a-z]$/.test(key)) { + return key.toUpperCase(); + } + + const aliases: Record = { + arrowDown: "Down", + arrowLeft: "Left", + arrowRight: "Right", + arrowUp: "Up", + backspace: "BackSpace", + delete: "Delete", + deleteForward: "Delete", + end: "End", + enter: "Return", + escape: "Escape", + home: "Home", + pageDown: "Page_Down", + pageUp: "Page_Up", + return: "Return", + space: "Space", + tab: "Tab", + }; + return aliases[key] || key; +} + +function formatKeyForHyprland(key: string): string { + if (/^[a-z]$/.test(key)) { + return key.toUpperCase(); + } + + const aliases: Record = { + arrowDown: "down", + arrowLeft: "left", + arrowRight: "right", + arrowUp: "up", + backspace: "backspace", + delete: "delete", + deleteForward: "delete", + enter: "return", + escape: "escape", + pageDown: "pagedown", + pageUp: "pageup", + return: "return", + space: "space", + tab: "tab", + }; + return aliases[key] || key; +} + +function formatKeyForI3Sway(key: string): string { + const aliases: Record = { + arrowDown: "Down", + arrowLeft: "Left", + arrowRight: "Right", + arrowUp: "Up", + backspace: "BackSpace", + delete: "Delete", + deleteForward: "Delete", + enter: "Return", + escape: "Escape", + pageDown: "Next", + pageUp: "Prior", + return: "Return", + space: "space", + tab: "Tab", + }; + return aliases[key] || key; +} + +function quoteKdlString(value: string): string { + return JSON.stringify(value); +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/g, "'\"'\"'")}'`; +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} + +const SHORTCUT_MODIFIER_ALIASES: Record = { + alt: "opt", + cmd: "cmd", + command: "cmd", + control: "ctrl", + ctrl: "ctrl", + meta: "cmd", + option: "opt", + opt: "opt", + shift: "shift", + super: "cmd", + win: "cmd", +}; + +const SHORTCUT_KEY_ALIASES: Record = { + backspace: "backspace", + delete: "delete", + deleteforward: "deleteForward", + down: "arrowDown", + enter: "enter", + esc: "escape", + escape: "escape", + left: "arrowLeft", + pagedown: "pageDown", + pageup: "pageUp", + plus: "+", + return: "return", + right: "arrowRight", + space: "space", + tab: "tab", + up: "arrowUp", +}; + +const SHORTCUT_KEYS = new Set([ + ..."abcdefghijklmnopqrstuvwxyz0123456789".split(""), + ".", + ",", + ";", + "=", + "+", + "-", + "[", + "]", + "{", + "}", + "(", + ")", + "/", + "\\", + "'", + "`", + "^", + "@", + "$", + "home", + "end", + "deleteForward", + "arrowUp", + "arrowDown", + "arrowLeft", + "arrowRight", + "pageUp", + "pageDown", + ...Object.values(SHORTCUT_KEY_ALIASES), +]); + +function normalizeShortcut(value: string): string | undefined { + const trimmed = value.trim(); + return trimmed ? trimmed.toLowerCase().replace(/\s+/g, "") : undefined; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return "Unknown error"; +} diff --git a/extensions/webapp-manager/tsconfig.json b/extensions/webapp-manager/tsconfig.json new file mode 100644 index 00000000..15aaa5d8 --- /dev/null +++ b/extensions/webapp-manager/tsconfig.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 16", + "include": ["src/**/*"], + "compilerOptions": { + //"lib": ["es2020"], + "module": "commonjs", + "target": "es2020", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "jsx": "react-jsx" + } +}