diff --git a/extensions/ray-tools/.editorconfig b/extensions/ray-tools/.editorconfig new file mode 100644 index 00000000000..192c070cf03 --- /dev/null +++ b/extensions/ray-tools/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/extensions/ray-tools/.gitattributes b/extensions/ray-tools/.gitattributes new file mode 100644 index 00000000000..d0d6371de32 --- /dev/null +++ b/extensions/ray-tools/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.png binary diff --git a/extensions/ray-tools/.gitignore b/extensions/ray-tools/.gitignore new file mode 100644 index 00000000000..92461ff8724 --- /dev/null +++ b/extensions/ray-tools/.gitignore @@ -0,0 +1,28 @@ +# Dependencies and generated output +node_modules/ +dist/ +coverage/ + +# Local configuration and secrets +.env +.env.* +!.env.example +.npmrc +!.npmrc.example +credentials*.json +secrets*.json +*.pem +*.key +*.p12 +*.pfx +id_rsa* +id_dsa* +id_ecdsa* +id_ed25519* + +# macOS and editor files +.DS_Store +.idea/ +.vscode/ +*.log +*.swp diff --git a/extensions/ray-tools/CHANGELOG.md b/extensions/ray-tools/CHANGELOG.md new file mode 100644 index 00000000000..6b5a2defd81 --- /dev/null +++ b/extensions/ray-tools/CHANGELOG.md @@ -0,0 +1,8 @@ +# ray-tools Changelog + +## [Initial Release] - {PR_MERGE_DATE} + +- Add Translate Text for Russian and English. +- Add Proofread Russian Text for spelling and punctuation checks. +- Add Clean Markdown for local plain-text cleanup of copied Markdown. +- Add Ping for network diagnostics. diff --git a/extensions/ray-tools/CONTRIBUTING.md b/extensions/ray-tools/CONTRIBUTING.md new file mode 100644 index 00000000000..49f5aedb47c --- /dev/null +++ b/extensions/ray-tools/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Thanks for helping improve Ray Tools. + +## Development setup + +1. Install Node.js 22.22.2 or newer. +2. Install dependencies with `npm ci`. +3. Run the complete local quality gate: + + ```bash + npm run check + ``` + +4. Run `npm run dev` to load the extension in Raycast while developing. + +## Project conventions + +- Keep tools independent under `src/tools/`. +- Keep domain logic free of Raycast imports. +- Put external integrations behind a small provider interface. +- Add or update unit tests for behavior changes. +- Do not commit `.env` files, credentials, private keys, personal data, or confidential translation samples. + +## Pull requests + +- Explain what changed and why. +- Keep changes focused and avoid unrelated reformatting. +- Include the commands used to verify the change, normally `npm run check`. +- Make sure the CI checks pass before requesting review. + +For security issues, follow [SECURITY.md](SECURITY.md) instead of opening a public issue. diff --git a/extensions/ray-tools/IDEA.md b/extensions/ray-tools/IDEA.md new file mode 100644 index 00000000000..b585e5f5778 --- /dev/null +++ b/extensions/ray-tools/IDEA.md @@ -0,0 +1,12 @@ +# Ideas + +Ray Tools is a collection of small Raycast tools for everyday work. + +Ideas should stay independent where possible: each tool should have a focused command, a small domain layer, and replaceable external providers. + +Possible next steps: + +- add a second translation provider, including an authenticated or self-hosted option; +- add more focused productivity tools without coupling them to the translator; +- improve error states and provider diagnostics; +- document privacy and provider trade-offs for every network-backed tool. diff --git a/extensions/ray-tools/LICENSE b/extensions/ray-tools/LICENSE new file mode 100644 index 00000000000..abcc897f783 --- /dev/null +++ b/extensions/ray-tools/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kirill + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/ray-tools/README.md b/extensions/ray-tools/README.md new file mode 100644 index 00000000000..97cf0139c34 --- /dev/null +++ b/extensions/ray-tools/README.md @@ -0,0 +1,99 @@ +# Ray Tools + +A small Raycast extension for focused, independent tools that make everyday work faster. + +> Early-stage project. The extension is useful today, but the API and feature set may evolve. + +## Features + +### Translate Text + +Translate selected text, clipboard contents, or text entered directly in Raycast. Russian and English are detected automatically and the direction is switched: + +- `ru → en` +- `en → ru` + +### Proofread Russian Text + +Check Russian spelling and punctuation in selected text, clipboard contents, or text entered directly in Raycast. The command shows each suggestion and a corrected version that can be copied or pasted. + +### Clean Markdown + +Remove common Markdown formatting from selected text, clipboard contents, or text entered directly in Raycast. Bold and italic markers, links, headings, quotes, lists, task markers, code fences, and inline code are converted into readable plain text that can be copied or pasted. + +### Ping + +Keep a menu-bar network status visible with a background check every 60 seconds. Ping checks the local router (macOS default gateway), packet loss to the internet host behind Google's HTTP 204 connectivity endpoint, a configurable HTTPS remote endpoint (default: `https://example.com`), and detectable macOS VPN activity. Its diagnosis is deliberately layered: it distinguishes likely local-network, ISP/internet-path, remote-server, and VPN problems, while showing `Inconclusive` when the probes do not prove a layer. A separate action runs macOS `networkQuality` for an explicit download-speed test of up to 8 seconds. + +## Privacy and configuration + +- Translation text is sent over HTTPS to Google's public translation endpoint. +- Russian proofreading text is sent over HTTPS to LanguageTool's public API. +- Markdown cleaning runs locally and does not send text to a provider. +- Ping sends only connectivity requests: five ICMP echoes to the local default gateway, five ICMP echoes to the internet host, and HTTPS requests to the configured public endpoints. The optional speed test uses macOS `networkQuality` and transfers test traffic. It does not send user text, credentials, or request data to those endpoints. +- The extension does not persist translation history or store user text locally. +- Ping keeps its current result in Raycast's running menu-bar command only; it does not persist the local gateway address or probe history. +- The current providers do not require an API key or any environment variables. +- Do not put credentials into source code or a `.env` file and assume they are hidden. A Raycast extension is client-side code, so a secret bundled into it can be extracted. An authenticated provider should use Raycast keychain-backed preferences or a server-side proxy instead. + +Do not send confidential or regulated data through the current public providers. + +## Requirements + +- macOS +- [Raycast](https://www.raycast.com/) +- Node.js 22.22.2+ and npm for development + +## Local development + +```bash +npm ci +npm run check +``` + +Run the extension in Raycast while developing: + +```bash +npm run dev +``` + +The `check` script runs the complete local quality gate: + +1. unit tests; +2. TypeScript type checking; +3. ESLint and Prettier through Raycast's linter; +4. a production extension build. + +## Architecture + +Each tool lives under `src/tools/` and is split into: + +- **domain** — pure language and request/response rules, with no Raycast imports; +- **providers** — external integrations behind a small interface; +- **UI command** — the Raycast adapter in `src/commands`. + +Shared infrastructure belongs in `src/shared`. New tools should add their own directory and manifest command without importing another tool's UI or provider. This keeps tools independently replaceable and makes it possible to add another translation backend without changing the user flow. + +## Translation backend + +The first version uses Google's public translation endpoint with automatic source-language detection. The endpoint is isolated behind `TranslationProvider`, so it can be replaced with an authenticated or self-hosted provider later without changing the user flow. + +The public endpoint may be rate-limited or change without notice. It is intentionally treated as a replaceable provider rather than a guaranteed production API. + +## Russian proofreading backend + +The Russian proofreading command uses LanguageTool's public `/v2/check` endpoint with the `ru-RU` language. It checks spelling, punctuation, grammar, and style rules, then applies the first suggestion for each non-overlapping issue to create the copyable corrected text. A small local Russian rule supplements the provider for context-sensitive spelling such as `не интересно` → `неинтересно` when there is no explicit negation or contrast. + +The public endpoint may be rate-limited or change without notice. Text sent to it should not contain confidential or regulated data. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and pull request expectations. Ideas and planned improvements are tracked in [IDEA.md](IDEA.md). + +## Security + +Please read [SECURITY.md](SECURITY.md) before reporting a vulnerability. Never publish credentials, private keys, personal data, or confidential translation samples in an issue. + +## License + +This project is released under the [MIT License](LICENSE). diff --git a/extensions/ray-tools/SECURITY.md b/extensions/ray-tools/SECURITY.md new file mode 100644 index 00000000000..0e7d893b216 --- /dev/null +++ b/extensions/ray-tools/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + +## Supported versions + +The latest state of the default branch is the only actively supported version while the project is in early development. + +## Reporting a vulnerability + +Please use GitHub's private vulnerability reporting or a private Security Advisory. Do not open a public issue for a suspected vulnerability. + +If private reporting is not enabled on the repository, contact the maintainer through a private channel listed on the maintainer's GitHub profile. Please include: + +- a concise description of the issue; +- affected files or versions; +- reproduction steps that do not contain real credentials or personal data; +- an assessment of potential impact. + +Please allow reasonable time for investigation and a fix before public disclosure. + +## Secret handling + +The current extension does not require API keys or environment variables. Translation text is sent to Google's public endpoint and Russian proofreading text is sent to LanguageTool's public API, so confidential data should not be submitted through either provider. + +If a credential is ever committed accidentally, revoke or rotate it first. Removing the file does not invalidate a secret that is already present in Git history. diff --git a/extensions/ray-tools/assets/icon.png b/extensions/ray-tools/assets/icon.png new file mode 100644 index 00000000000..f03c362ac34 Binary files /dev/null and b/extensions/ray-tools/assets/icon.png differ diff --git a/extensions/ray-tools/eslint.config.js b/extensions/ray-tools/eslint.config.js new file mode 100644 index 00000000000..543274e97ec --- /dev/null +++ b/extensions/ray-tools/eslint.config.js @@ -0,0 +1,4 @@ +const { defineConfig } = require("eslint/config"); +const raycastConfig = require("@raycast/eslint-config"); + +module.exports = defineConfig([...raycastConfig]); diff --git a/extensions/ray-tools/metadata/ray-tools-1.png b/extensions/ray-tools/metadata/ray-tools-1.png new file mode 100644 index 00000000000..592a1ba95bf Binary files /dev/null and b/extensions/ray-tools/metadata/ray-tools-1.png differ diff --git a/extensions/ray-tools/metadata/ray-tools-2.png b/extensions/ray-tools/metadata/ray-tools-2.png new file mode 100644 index 00000000000..1f6f4a826f2 Binary files /dev/null and b/extensions/ray-tools/metadata/ray-tools-2.png differ diff --git a/extensions/ray-tools/metadata/ray-tools-3.png b/extensions/ray-tools/metadata/ray-tools-3.png new file mode 100644 index 00000000000..713189a1aa3 Binary files /dev/null and b/extensions/ray-tools/metadata/ray-tools-3.png differ diff --git a/extensions/ray-tools/package-lock.json b/extensions/ray-tools/package-lock.json new file mode 100644 index 00000000000..4cdd20d1552 --- /dev/null +++ b/extensions/ray-tools/package-lock.json @@ -0,0 +1,4237 @@ +{ + "name": "ray-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ray-tools", + "license": "MIT", + "dependencies": { + "@raycast/api": "^1.104.24" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.2.0", + "@types/node": "^26.1.2", + "@types/react": "^19.0.10", + "@typescript/native": "npm:typescript@^7.0.2", + "eslint": "^10.8.1", + "prettier": "^3.5.3", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "vitest": "^4.0.8" + }, + "engines": { + "node": ">=22.22.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oclif/core": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.13.3.tgz", + "integrity": "sha512-wBp2igI2KVwq2ZmpnfmGtOROo7aXZIr3OEtAS16g1wHFBqyEuswL+9K50NJWJvlpAmxd+4dVj/ycawXESU3wQQ==", + "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": "^10.2.5", + "semver": "^7.8.1", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.17", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.56", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.56.tgz", + "integrity": "sha512-P96b6RZvQatlJ6pX6sgIFOQhgX2tFVhgSfw53GT/MajecCQNFGVqagJKo2CfcPsacDpuFuPRT6zji8h1oaqtrA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.1", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.58", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.58.tgz", + "integrity": "sha512-DAYMXZrTWRMLTbpX+rT+ibAmKrZ6IVNGn+fgAGjMoeNlqlSY94eJHocgmqChMwsdWp3H3x+jFmPJiYQt/ifr3Q==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "3.2.93", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.93.tgz", + "integrity": "sha512-KBizxn+kgTPWLUCo/vDz/oDdEA5Ll5H6x0jJhl05B56yHwgQp46ZxwXmtBsxvvgrmMqJEtFUw2Qrq+nTHWWv4g==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^4.13.3", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@raycast/api": { + "version": "1.104.24", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.104.24.tgz", + "integrity": "sha512-Jppfyi1T7wcGXZFxpzXuvNAKUPQ+BQkKC0N3rJBz1epp/z22SH9aLA23YZRk/ickGwEm6DI/7OY7Jin8dD53Sg==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.8.4", + "@oclif/plugin-autocomplete": "^3.2.40", + "@oclif/plugin-help": "^6.2.37", + "@oclif/plugin-not-found": "^3.2.74", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "esbuild": "^0.27.3", + "react": "19.0.0" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=22.22.2" + }, + "peerDependencies": { + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "react-devtools": "6.1.1" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/api/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@raycast/api/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/@raycast/eslint-config": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.2.0.tgz", + "integrity": "sha512-uS/UDvM+3HfABgYhLYnIv0y+IVu+rGiZZIMwQsO5EGrcf2/TX/yioTzfezAQqhdszW4ViCuHbv8BddIagDKZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/js": "^9.39.4", + "@raycast/eslint-plugin": "^2.2.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.7.0", + "typescript-eslint": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "prettier": ">=2", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.2.0.tgz", + "integrity": "sha512-ixO3PoRAEAZZcSQeRPvvDS/vs09D47tXdyXrs7hctoxxPtweXgJ6I24kjCD5GuuuQS5hGTiuL+Uy6hIg6a/a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript/native": { + "name": "typescript", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/@typescript/old": { + "name": "typescript", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/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": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "license": "MIT" + }, + "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/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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/eslint": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-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": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.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": { + "name": "@typescript/typescript6", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript6/-/typescript6-6.0.2.tgz", + "integrity": "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript/old": "npm:typescript@^6" + }, + "bin": { + "tsc6": "bin/tsc6" + } + }, + "node_modules/typescript-eslint": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/extensions/ray-tools/package.json b/extensions/ray-tools/package.json new file mode 100644 index 00000000000..3269127175f --- /dev/null +++ b/extensions/ray-tools/package.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "ray-tools", + "title": "Ray Tools", + "description": "A focused set of independent tools for everyday work", + "icon": "icon.png", + "author": "kirill", + "categories": [ + "Productivity" + ], + "license": "MIT", + "engines": { + "node": ">=22.22.2" + }, + "platforms": [ + "macOS" + ], + "commands": [ + { + "name": "translate", + "title": "Translate Text", + "subtitle": "RU ↔ EN", + "description": "Translate selected or typed text between Russian and English", + "mode": "view" + }, + { + "name": "proofread-russian", + "title": "Proofread Russian Text", + "subtitle": "Spelling & Punctuation", + "description": "Check Russian spelling and punctuation in selected or typed text", + "mode": "view" + }, + { + "name": "clean-markdown", + "title": "Очистить Markdown", + "subtitle": "Убрать форматирование", + "description": "Убрать Markdown-разметку из выделенного или введённого текста", + "mode": "view" + }, + { + "name": "ping", + "title": "Пинг", + "subtitle": "Состояние сети", + "description": "Постоянно проверять локальную сеть, интернет, удалённый сервер и VPN", + "mode": "menu-bar", + "interval": "1m", + "preferences": [ + { + "name": "remoteEndpoint", + "title": "Удалённый адрес", + "description": "HTTPS-адрес для отличия проблемы удалённого сервера от общей проблемы с интернетом", + "type": "textfield", + "required": false, + "default": "https://example.com", + "placeholder": "https://example.com/health" + } + ] + } + ], + "dependencies": { + "@raycast/api": "^1.104.24" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.2.0", + "@types/node": "^26.1.2", + "@types/react": "^19.0.10", + "@typescript/native": "npm:typescript@^7.0.2", + "eslint": "^10.8.1", + "prettier": "^3.5.3", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "vitest": "^4.0.8" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "check": "npm run test && npm run typecheck && npm run lint && npm run build", + "publish": "ray publish" + } +} diff --git a/extensions/ray-tools/src/clean-markdown.tsx b/extensions/ray-tools/src/clean-markdown.tsx new file mode 100644 index 00000000000..36973cc42fe --- /dev/null +++ b/extensions/ray-tools/src/clean-markdown.tsx @@ -0,0 +1 @@ +export { default } from "./commands/clean-markdown"; diff --git a/extensions/ray-tools/src/commands/clean-markdown.tsx b/extensions/ray-tools/src/commands/clean-markdown.tsx new file mode 100644 index 00000000000..2dce00f5666 --- /dev/null +++ b/extensions/ray-tools/src/commands/clean-markdown.tsx @@ -0,0 +1,164 @@ +import { + Action, + ActionPanel, + Clipboard, + Detail, + Form, + getSelectedText, + Icon, + showToast, + Toast, +} from "@raycast/api"; +import { useCallback, useEffect, useState } from "react"; + +import { cleanMarkdown } from "../tools/clean-markdown/domain"; +import { formatResult } from "../tools/clean-markdown/markdown"; + +type CleanMarkdownFormValues = { + text: string; +}; + +function getErrorMessage(error: unknown): string { + return error instanceof Error + ? error.message + : "Не удалось очистить Markdown"; +} + +export default function CleanMarkdownCommand() { + const [sourceText, setSourceText] = useState(""); + const [cleanedText, setCleanedText] = useState(); + const [error, setError] = useState(); + + const cleanText = useCallback((text: string) => { + const normalizedText = text.trim(); + setSourceText(normalizedText); + setCleanedText(undefined); + setError(undefined); + + if (!normalizedText) { + const message = "Введите текст или скопируйте его в буфер обмена"; + setError(message); + void showToast({ style: Toast.Style.Failure, title: message }); + return; + } + + try { + setCleanedText(cleanMarkdown(normalizedText)); + void showToast({ + style: Toast.Style.Success, + title: "Markdown очищен", + }); + } catch (cleanError) { + const message = getErrorMessage(cleanError); + setError(message); + void showToast({ + style: Toast.Style.Failure, + title: "Не удалось очистить Markdown", + message, + }); + } + }, []); + + useEffect(() => { + let active = true; + + getSelectedText() + .then((selectedText) => { + if (active && selectedText.trim()) { + cleanText(selectedText); + } + }) + .catch(() => { + // No selected text is a normal way to open the command. + }); + + return () => { + active = false; + }; + }, [cleanText]); + + if (cleanedText !== undefined) { + return ( + + + + { + setCleanedText(undefined); + setError(undefined); + }} + /> + cleanText(sourceText)} + /> + + } + /> + ); + } + + return ( +
+ + cleanText(values.text) + } + /> + { + const clipboardText = await Clipboard.readText(); + if (clipboardText?.trim()) { + cleanText(clipboardText); + } else { + await showToast({ + style: Toast.Style.Failure, + title: "Буфер обмена пуст", + }); + } + }} + /> + + } + > + + + + ); +} diff --git a/extensions/ray-tools/src/commands/ping.tsx b/extensions/ray-tools/src/commands/ping.tsx new file mode 100644 index 00000000000..3cd872ace05 --- /dev/null +++ b/extensions/ray-tools/src/commands/ping.tsx @@ -0,0 +1,272 @@ +import { + Color, + getPreferenceValues, + Icon, + Keyboard, + MenuBarExtra, +} from "@raycast/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { getProbeDetail } from "../tools/ping/domain"; +import { + MacNetworkPingProvider, + normalizeRemoteEndpoint, +} from "../tools/ping/provider"; +import { PingService } from "../tools/ping/service"; +import type { PingProbeResult, PingResult } from "../tools/ping/types"; + +type StatusPresentation = { + title: string; + summary: string; + icon: Icon; + color: Color; +}; + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Не удалось проверить сеть"; +} + +function getStatusPresentation( + diagnosis: PingResult["diagnosis"] | undefined, + error: string | undefined, + isLoading: boolean, +): StatusPresentation { + if (diagnosis) { + switch (diagnosis.code) { + case "healthy": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.Checkmark, + color: Color.Green, + }; + case "local-network": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.WifiDisabled, + color: Color.Red, + }; + case "isp-or-internet": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.Globe, + color: Color.Red, + }; + case "remote-server": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.Network, + color: Color.Orange, + }; + case "vpn": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.Shield, + color: Color.Orange, + }; + case "inconclusive": + return { + title: diagnosis.title, + summary: diagnosis.summary, + icon: Icon.QuestionMarkCircle, + color: Color.Yellow, + }; + } + } + + return { + title: isLoading ? "Проверка…" : "Неизвестно", + summary: error ?? "Ожидание первой проверки сети.", + icon: isLoading ? Icon.CircleProgress : Icon.QuestionMarkCircle, + color: Color.Yellow, + }; +} + +function getProbeIcon(probe: PingProbeResult): { + source: Icon; + tintColor: Color; +} { + switch (probe.state) { + case "pass": + return { source: Icon.Checkmark, tintColor: Color.Green }; + case "fail": + return { source: Icon.XMarkCircle, tintColor: Color.Red }; + case "unknown": + return { source: Icon.QuestionMarkCircle, tintColor: Color.Yellow }; + case "not-detected": + return { source: Icon.Circle, tintColor: Color.SecondaryText }; + } +} + +function formatCheckedAt(checkedAt: string): string { + const date = new Date(checkedAt); + if (Number.isNaN(date.getTime())) { + return "Неизвестно"; + } + + return date.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +function getTooltip( + presentation: StatusPresentation, + result: PingResult | undefined, + isLoading: boolean, +): string { + const checkedAt = result + ? `Последняя проверка: ${formatCheckedAt(result.checkedAt)}` + : "Проверка ещё не завершена"; + const refreshing = isLoading ? "\nОбновление…" : ""; + return `${presentation.title}: ${presentation.summary}\n${checkedAt}${refreshing}`; +} + +export default function PingCommand() { + const preferences = getPreferenceValues(); + const remoteEndpoint = normalizeRemoteEndpoint(preferences.remoteEndpoint); + const service = useMemo(() => { + const provider = new MacNetworkPingProvider({ remoteEndpoint }); + return { provider, service: new PingService(provider) }; + }, [remoteEndpoint]); + const refreshRequestNumber = useRef(0); + const speedRequestNumber = useRef(0); + const [result, setResult] = useState(); + const [error, setError] = useState(); + const [isRefreshing, setIsRefreshing] = useState(true); + const [isSpeedLoading, setIsSpeedLoading] = useState(false); + + const refresh = useCallback(async () => { + const currentRequest = ++refreshRequestNumber.current; + setIsRefreshing(true); + setError(undefined); + + try { + const nextResult = await service.service.check(); + if (currentRequest !== refreshRequestNumber.current) { + return; + } + + setResult(nextResult); + } catch (checkError) { + if (currentRequest !== refreshRequestNumber.current) { + return; + } + + setResult(undefined); + setError(getErrorMessage(checkError)); + } finally { + if (currentRequest === refreshRequestNumber.current) { + setIsRefreshing(false); + } + } + }, [service]); + + const measureSpeed = useCallback(async () => { + const currentRequest = ++speedRequestNumber.current; + setIsSpeedLoading(true); + setError(undefined); + + try { + const speed = await service.provider.measureSpeed(); + if (currentRequest !== speedRequestNumber.current) { + return; + } + setResult((current) => (current ? { ...current, speed } : current)); + } catch (speedError) { + if (currentRequest === speedRequestNumber.current) { + setError(getErrorMessage(speedError)); + } + } finally { + if (currentRequest === speedRequestNumber.current) { + setIsSpeedLoading(false); + } + } + }, [service]); + + useEffect(() => { + void refresh(); + }, [refresh]); + + const presentation = getStatusPresentation( + result?.diagnosis, + error, + isRefreshing || isSpeedLoading, + ); + const probes = result + ? [result.gateway, result.internet, result.speed, result.server, result.vpn] + : []; + + return ( + + + + + + + {probes.length > 0 ? ( + probes.map((probe) => ( + + )) + ) : ( + + )} + + + {result ? ( + + + + ) : null} + + + void measureSpeed()} + /> + void refresh()} + /> + + + ); +} diff --git a/extensions/ray-tools/src/commands/proofread-russian.tsx b/extensions/ray-tools/src/commands/proofread-russian.tsx new file mode 100644 index 00000000000..5e091846aaa --- /dev/null +++ b/extensions/ray-tools/src/commands/proofread-russian.tsx @@ -0,0 +1,305 @@ +import { + Action, + ActionPanel, + Clipboard, + Color, + Form, + getSelectedText, + Icon, + List, + showToast, + Toast, +} from "@raycast/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { + formatCorrectedText, + formatIssueDetail, + formatIssueTitle, + getIssueCategoryLabel, +} from "../tools/proofread-russian/markdown"; +import { LanguageToolProvider } from "../tools/proofread-russian/provider"; +import { RussianProofreadingService } from "../tools/proofread-russian/service"; +import type { + ProofreadingIssue, + ProofreadingResult, +} from "../tools/proofread-russian/types"; + +type ProofreadingFormValues = { + text: string; +}; + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Proofreading failed"; +} + +function getIssueTintColor(category: ProofreadingIssue["category"]): Color { + return category === "punctuation" ? Color.Blue : Color.Red; +} + +export default function ProofreadRussianCommand() { + const service = useMemo( + () => new RussianProofreadingService(new LanguageToolProvider()), + [], + ); + const requestNumber = useRef(0); + const [sourceText, setSourceText] = useState(""); + const [result, setResult] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + + const checkText = useCallback( + async (text: string) => { + const currentRequest = ++requestNumber.current; + setSourceText(text); + setResult(undefined); + setError(undefined); + setIsLoading(true); + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Checking Russian text…", + }); + + try { + const proofreadingResult = await service.check(text); + if (currentRequest !== requestNumber.current) { + return; + } + + setSourceText(proofreadingResult.text); + setResult(proofreadingResult); + toast.style = Toast.Style.Success; + toast.title = + proofreadingResult.issues.length === 0 + ? "No issues found" + : `${proofreadingResult.issues.length} ${proofreadingResult.issues.length === 1 ? "issue" : "issues"} found`; + } catch (proofreadingError) { + if (currentRequest !== requestNumber.current) { + return; + } + + const message = getErrorMessage(proofreadingError); + setError(message); + toast.style = Toast.Style.Failure; + toast.title = "Proofreading failed"; + toast.message = message; + } finally { + if (currentRequest === requestNumber.current) { + setIsLoading(false); + } + } + }, + [service], + ); + + useEffect(() => { + let active = true; + + getSelectedText() + .then((selectedText) => { + if (active && selectedText.trim()) { + void checkText(selectedText); + } + }) + .catch(() => { + // No selected text is a normal way to open the command; keep the form empty. + }); + + return () => { + active = false; + }; + }, [checkText]); + + if (result) { + return ( + + 0 + ? `Issues (${result.issues.length})` + : "Result" + } + > + {result.issues.length > 0 ? ( + result.issues.map((issue, index) => ( + + } + actions={ + + + + { + setResult(undefined); + setError(undefined); + }} + /> + void checkText(result.text)} + /> + + } + /> + )) + ) : ( + + } + actions={ + + + + { + setResult(undefined); + setError(undefined); + }} + /> + + } + /> + )} + + {result.issues.length > 0 ? ( + + + } + actions={ + + + + { + setResult(undefined); + setError(undefined); + }} + /> + void checkText(result.text)} + /> + + } + /> + + ) : null} + + ); + } + + return ( +
+ + void checkText(values.text) + } + /> + { + const clipboardText = await Clipboard.readText(); + if (clipboardText?.trim()) { + void checkText(clipboardText); + } else { + await showToast({ + style: Toast.Style.Failure, + title: "Clipboard is empty", + }); + } + }} + /> + + } + > + + + + ); +} diff --git a/extensions/ray-tools/src/commands/translate.tsx b/extensions/ray-tools/src/commands/translate.tsx new file mode 100644 index 00000000000..8880ec5cff1 --- /dev/null +++ b/extensions/ray-tools/src/commands/translate.tsx @@ -0,0 +1,184 @@ +import { + Action, + ActionPanel, + Clipboard, + Detail, + Form, + getSelectedText, + Icon, + showToast, + Toast, +} from "@raycast/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { GoogleTranslateProvider } from "../tools/translate/provider"; +import { TranslationService } from "../tools/translate/service"; +import { formatResult } from "../tools/translate/markdown"; +import type { TranslationResult } from "../tools/translate/types"; + +type TranslateFormValues = { + text: string; +}; + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Translation failed"; +} + +export default function TranslateCommand() { + const service = useMemo( + () => new TranslationService(new GoogleTranslateProvider()), + [], + ); + const requestNumber = useRef(0); + const [sourceText, setSourceText] = useState(""); + const [result, setResult] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + + const translate = useCallback( + async (text: string) => { + const currentRequest = ++requestNumber.current; + setSourceText(text); + setResult(undefined); + setError(undefined); + setIsLoading(true); + + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Translating…", + }); + + try { + const translation = await service.translate(text); + if (currentRequest !== requestNumber.current) { + return; + } + + setResult(translation); + toast.style = Toast.Style.Success; + toast.title = "Translation ready"; + } catch (translationError) { + if (currentRequest !== requestNumber.current) { + return; + } + + const message = getErrorMessage(translationError); + setError(message); + toast.style = Toast.Style.Failure; + toast.title = "Translation failed"; + toast.message = message; + } finally { + if (currentRequest === requestNumber.current) { + setIsLoading(false); + } + } + }, + [service], + ); + + useEffect(() => { + let active = true; + + getSelectedText() + .then((selectedText) => { + if (active && selectedText.trim()) { + void translate(selectedText); + } + }) + .catch(() => { + // No selected text is a normal way to open the command; keep the form empty. + }); + + return () => { + active = false; + }; + }, [translate]); + + if (result) { + return ( + + + + { + setResult(undefined); + setError(undefined); + }} + /> + void translate(sourceText)} + /> + + } + /> + ); + } + + return ( +
+ + void translate(values.text) + } + /> + { + const clipboardText = await Clipboard.readText(); + if (clipboardText?.trim()) { + void translate(clipboardText); + } else { + await showToast({ + style: Toast.Style.Failure, + title: "Clipboard is empty", + }); + } + }} + /> + + } + > + + + + ); +} diff --git a/extensions/ray-tools/src/ping.tsx b/extensions/ray-tools/src/ping.tsx new file mode 100644 index 00000000000..4b31951e4c7 --- /dev/null +++ b/extensions/ray-tools/src/ping.tsx @@ -0,0 +1 @@ +export { default } from "./commands/ping"; diff --git a/extensions/ray-tools/src/proofread-russian.tsx b/extensions/ray-tools/src/proofread-russian.tsx new file mode 100644 index 00000000000..94045ceb6a5 --- /dev/null +++ b/extensions/ray-tools/src/proofread-russian.tsx @@ -0,0 +1 @@ +export { default } from "./commands/proofread-russian"; diff --git a/extensions/ray-tools/src/tools/clean-markdown/domain.test.ts b/extensions/ray-tools/src/tools/clean-markdown/domain.test.ts new file mode 100644 index 00000000000..271eaae533b --- /dev/null +++ b/extensions/ray-tools/src/tools/clean-markdown/domain.test.ts @@ -0,0 +1,37 @@ +import { cleanMarkdown } from "./domain"; + +describe("Markdown cleaning", () => { + it("removes inline formatting but keeps ordinary punctuation", () => { + expect( + cleanMarkdown( + "**Жирный** и *курсив*, ~~зачёркнутый~~, `const value = 1` и 2 * 3.", + ), + ).toBe("Жирный и курсив, зачёркнутый, const value = 1 и 2 * 3."); + }); + + it("removes headings, quotes, list syntax, and task markers", () => { + expect( + cleanMarkdown( + "# Заголовок\n\n> Цитата\n\n- [x] Готово\n- [ ] Сделать\n1. Первый пункт", + ), + ).toBe("Заголовок\n\nЦитата\n\n☑ Готово\n☐ Сделать\n1) Первый пункт"); + }); + + it("keeps useful content from links and removes code fences", () => { + expect( + cleanMarkdown( + "[Документация](https://example.com/docs)\n\n```ts\nconst value = **1**;\n```", + ), + ).toBe("Документация (https://example.com/docs)\n\nconst value = **1**;"); + }); + + it("respects escaped Markdown characters and underscores in words", () => { + expect(cleanMarkdown("\\*не форматирование\\* и snake_case")).toBe( + "*не форматирование* и snake_case", + ); + }); + + it("removes excessive blank lines and horizontal rules", () => { + expect(cleanMarkdown("До\n\n\n---\n\nПосле")).toBe("До\n\nПосле"); + }); +}); diff --git a/extensions/ray-tools/src/tools/clean-markdown/domain.ts b/extensions/ray-tools/src/tools/clean-markdown/domain.ts new file mode 100644 index 00000000000..fa25cccf034 --- /dev/null +++ b/extensions/ray-tools/src/tools/clean-markdown/domain.ts @@ -0,0 +1,101 @@ +const MARKDOWN_LINK_PATTERN = /!?\[([^\]]+)\]\((\S+?)(?:\s+["'][^)]*["'])?\)/gu; +const AUTOLINK_PATTERN = /<((?:https?:\/\/|mailto:)[^>\s]+)>/giu; +const INLINE_CODE_PATTERN = /`([^`\n]+)`/gu; +const ESCAPED_MARKDOWN_PATTERN = + /\\(\\|`|\*|_|\[|\]|\{|\}|\(|\)|#|\+|\.|!|\||>|~|-)/gu; +const FENCE_PATTERN = /^\s*(`{3,}|~{3,})/u; +const MARKDOWN_PLACEHOLDER_PATTERN = /\uE000(\d+)\uE000/gu; + +function cleanInline(text: string): string { + const protectedValues: string[] = []; + const protect = (value: string): string => { + const index = protectedValues.push(value) - 1; + return `\uE000${index}\uE000`; + }; + + const cleaned = text + .replace(MARKDOWN_LINK_PATTERN, (_match, label: string, url: string) => { + const cleanedLabel = cleanInline(label); + return protect(cleanedLabel ? `${cleanedLabel} (${url})` : url); + }) + .replace(AUTOLINK_PATTERN, (_match, value: string) => protect(value)) + .replace(INLINE_CODE_PATTERN, (_match, value: string) => protect(value)) + .replace(ESCAPED_MARKDOWN_PATTERN, (_match, value: string) => + protect(value), + ) + .replace(/<\/?[A-Za-z][^>]*>/gu, "") + .replace( + /(? { + return protectedValues[Number(index)] ?? match; + }, + ); +} + +function cleanLine(line: string): string { + const cleaned = line + .replace(/^\s{0,3}(?:>\s?)+/u, "") + .replace(/^\s{0,3}#{1,6}(?:\s+|$)/u, "") + .replace(/\s+#{1,6}\s*$/u, ""); + + if (/^\s*(?:[-*_]\s*){3,}$/u.test(cleaned)) { + return ""; + } + + const task = cleaned.match(/^(\s*)[-+*]\s+\[([ xX])\]\s+(.*)$/u); + if (task) { + const marker = task[2].toLowerCase() === "x" ? "☑" : "☐"; + return `${task[1]}${marker} ${cleanInline(task[3])}`; + } + + const unordered = cleaned.match(/^(\s*)[-+*]\s+(.*)$/u); + if (unordered) { + return `${unordered[1]}• ${cleanInline(unordered[2])}`; + } + + const ordered = cleaned.match(/^(\s*)(\d+)[.)]\s+(.*)$/u); + if (ordered) { + return `${ordered[1]}${ordered[2]}) ${cleanInline(ordered[3])}`; + } + + const reference = cleaned.match(/^\s*\[[^\]]+\]:\s*(\S+)\s*$/u); + if (reference) { + return reference[1]; + } + + return cleanInline(cleaned); +} + +/** Remove common Markdown syntax while keeping the readable text. */ +export function cleanMarkdown(text: string): string { + let inFence = false; + const lines = text.replace(/\r\n?/gu, "\n").split("\n"); + const cleanedLines = lines.map((line) => { + if (FENCE_PATTERN.test(line)) { + inFence = !inFence; + return ""; + } + + return inFence ? line : cleanLine(line); + }); + + return cleanedLines + .join("\n") + .replace(/[ \t]+\n/gu, "\n") + .replace(/\n{3,}/gu, "\n\n") + .trim(); +} diff --git a/extensions/ray-tools/src/tools/clean-markdown/markdown.ts b/extensions/ray-tools/src/tools/clean-markdown/markdown.ts new file mode 100644 index 00000000000..68b79d19856 --- /dev/null +++ b/extensions/ray-tools/src/tools/clean-markdown/markdown.ts @@ -0,0 +1,33 @@ +function longestBacktickRun(text: string): number { + let longest = 0; + let current = 0; + + for (const character of text) { + if (character === "`") { + current += 1; + continue; + } + + longest = Math.max(longest, current); + current = 0; + } + + return Math.max(longest, current); +} + +function asMarkdownCodeBlock(text: string): string { + const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1)); + return `${fence}\n${text}\n${fence}`; +} + +export function formatResult(cleanedText: string, sourceText: string): string { + return [ + "## Очищенный текст", + "", + asMarkdownCodeBlock(cleanedText), + "", + "### Было", + "", + asMarkdownCodeBlock(sourceText), + ].join("\n"); +} diff --git a/extensions/ray-tools/src/tools/ping/domain.test.ts b/extensions/ray-tools/src/tools/ping/domain.test.ts new file mode 100644 index 00000000000..1c72e5f397c --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/domain.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from "vitest"; + +import { diagnosePing, getProbeDetail, getProbeStateLabel } from "./domain"; +import type { PingProbeResult, PingProbeSet } from "./types"; + +function probe( + id: PingProbeResult["id"], + state: PingProbeResult["state"], +): PingProbeResult { + return { + id, + label: id, + state, + detail: `${id} detail`, + }; +} + +function probes( + gateway: PingProbeResult["state"], + internet: PingProbeResult["state"], + server: PingProbeResult["state"], + vpn: PingProbeResult["state"] = "not-detected", +): PingProbeSet { + return { + gateway: probe("gateway", gateway), + internet: probe("internet", internet), + server: probe("server", server), + vpn: probe("vpn", vpn), + speed: probe("speed", "pass"), + }; +} + +describe("Ping diagnosis", () => { + it("reports a healthy path when every required layer responds", () => { + expect(diagnosePing(probes("pass", "pass", "pass"))).toMatchObject({ + code: "healthy", + title: "В сети", + }); + }); + + it("accepts an online VPN path when the tunnel has no gateway address", () => { + expect( + diagnosePing(probes("unknown", "pass", "pass", "pass")), + ).toMatchObject({ + code: "healthy", + summary: + "Точка проверки интернета и удалённый сервер доступны через обнаруженный VPN-маршрут.", + }); + }); + + it("points to the local network when the gateway and internet fail", () => { + expect(diagnosePing(probes("fail", "fail", "fail"))).toMatchObject({ + code: "local-network", + }); + }); + + it("points to the VPN when it is active during an internet failure", () => { + expect(diagnosePing(probes("pass", "fail", "fail", "pass"))).toMatchObject({ + code: "vpn", + }); + }); + + it("points beyond the local network when only the internet probe fails", () => { + expect(diagnosePing(probes("pass", "fail", "fail"))).toMatchObject({ + code: "isp-or-internet", + }); + }); + + it("does not blame the ISP when VPN inspection is unavailable", () => { + expect( + diagnosePing(probes("pass", "fail", "fail", "unknown")), + ).toMatchObject({ + code: "inconclusive", + }); + }); + + it("points to the remote server when the internet works but it fails", () => { + expect(diagnosePing(probes("pass", "pass", "fail"))).toMatchObject({ + code: "remote-server", + }); + }); + + it("attributes gateway packet loss to the local network", () => { + const result = probes("pass", "pass", "pass"); + result.gateway.packetLossPercent = 20; + + expect(diagnosePing(result)).toMatchObject({ code: "local-network" }); + }); + + it("attributes internet packet loss beyond a clean gateway to the ISP path", () => { + const result = probes("pass", "pass", "pass"); + result.gateway.packetLossPercent = 0; + result.internet.packetLossPercent = 20; + + expect(diagnosePing(result)).toMatchObject({ code: "isp-or-internet" }); + }); + + it("stays honest when a local probe fails but the internet works", () => { + expect(diagnosePing(probes("fail", "pass", "pass"))).toMatchObject({ + code: "inconclusive", + }); + }); +}); + +describe("Ping probe labels", () => { + it("formats state, latency, and detail for the menu", () => { + expect(getProbeStateLabel("not-detected")).toBe("Не обнаружено"); + expect( + getProbeDetail({ + ...probe("internet", "pass"), + latencyMs: 12.5, + }), + ).toBe("Работает · 12,5 мс · internet detail"); + }); + + it("shows packet loss and download throughput when measured", () => { + expect( + getProbeDetail({ + ...probe("internet", "pass"), + latencyMs: 15.625, + packetLossPercent: 20, + downloadMbps: 84.3, + }), + ).toBe( + "Работает · 15,625 мс · Потери 20% · ↓ 84,3 Мбит/с · internet detail", + ); + }); + + it("calls an unmeasured download speed a measurement state, not a failure", () => { + expect( + getProbeDetail({ + ...probe("speed", "not-detected"), + detail: + "Нажмите «Измерить скорость скачивания», чтобы узнать результат.", + }), + ).toBe( + "Не измерено · Нажмите «Измерить скорость скачивания», чтобы узнать результат.", + ); + }); + + it("explains an unavailable router without calling it a network failure", () => { + expect( + getProbeDetail({ + ...probe("gateway", "unknown"), + detail: "Роутер не найден. В VPN-режиме это может быть нормально.", + }), + ).toBe( + "Не определён · Роутер не найден. В VPN-режиме это может быть нормально.", + ); + }); +}); diff --git a/extensions/ray-tools/src/tools/ping/domain.ts b/extensions/ray-tools/src/tools/ping/domain.ts new file mode 100644 index 00000000000..648d5508766 --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/domain.ts @@ -0,0 +1,150 @@ +import type { + PingDiagnosis, + PingProbeResult, + PingProbeSet, + ProbeState, +} from "./types"; + +export function getProbeStateLabel(state: ProbeState): string { + switch (state) { + case "pass": + return "Работает"; + case "fail": + return "Ошибка"; + case "not-detected": + return "Не обнаружено"; + case "unknown": + return "Неизвестно"; + } +} + +export function diagnosePing(probes: PingProbeSet): PingDiagnosis { + const { gateway, internet, server, vpn } = probes; + const localPathConfirmedByVpn = + gateway.state === "unknown" && vpn.state === "pass"; + + if ((gateway.packetLossPercent ?? 0) > 0) { + return { + code: "local-network", + title: "Потери пакетов в локальной сети", + summary: `${gateway.packetLossPercent}% пакетов теряется между Mac и роутером.`, + }; + } + + if ((internet.packetLossPercent ?? 0) > 0) { + return { + code: "isp-or-internet", + title: "Потери пакетов в интернете", + summary: `${internet.packetLossPercent}% пакетов теряется после нормального соединения с роутером.`, + }; + } + + if (internet.state === "pass") { + if (server.state === "fail") { + return { + code: "remote-server", + title: "Удалённый сервер", + summary: + "Интернет доступен, но настроенный удалённый сервер не отвечает.", + }; + } + + if ( + server.state !== "pass" || + (gateway.state !== "pass" && !localPathConfirmedByVpn) + ) { + return { + code: "inconclusive", + title: "Нельзя точно определить", + summary: + "Интернет доступен, но локальная или удалённая проверка не подтвердила весь маршрут.", + }; + } + + return { + code: "healthy", + title: "В сети", + summary: localPathConfirmedByVpn + ? "Точка проверки интернета и удалённый сервер доступны через обнаруженный VPN-маршрут." + : "Роутер, интернет и удалённый сервер доступны.", + }; + } + + if (internet.state === "fail") { + if (vpn.state === "pass") { + return { + code: "vpn", + title: "Маршрут через VPN", + summary: + "Проверка интернета не удалась, при этом обнаружена активность VPN или VPN-маршрут.", + }; + } + + if (vpn.state === "unknown") { + return { + code: "inconclusive", + title: "Нельзя точно определить", + summary: + "Проверка интернета не удалась, а состояние VPN проверить не удалось — определить проблемный участок нельзя.", + }; + } + + if (gateway.state === "fail") { + return { + code: "local-network", + title: "Локальная сеть", + summary: + "Нет связи с роутером и интернетом; проверьте Wi-Fi, Ethernet или сам роутер.", + }; + } + + if (gateway.state === "pass") { + return { + code: "isp-or-internet", + title: "Провайдер / интернет", + summary: + "Роутер отвечает, но интернет — нет; проблема за пределами локальной сети или у провайдера.", + }; + } + } + + if (gateway.state !== "pass" || server.state !== "pass") { + return { + code: "inconclusive", + title: "Нельзя точно определить", + summary: + "Некоторые проверки не завершились, поэтому определить проблемный участок нельзя.", + }; + } + + return { + code: "inconclusive", + title: "Нельзя точно определить", + summary: "Проверка сети не дала достаточно данных для диагноза.", + }; +} + +export function getProbeDetail(probe: PingProbeResult): string { + const state = + probe.id === "gateway" && probe.state === "unknown" + ? "Не определён" + : probe.id === "speed" && probe.state === "not-detected" + ? "Не измерено" + : getProbeStateLabel(probe.state); + const latency = + probe.latencyMs === undefined + ? undefined + : `${probe.latencyMs.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс`; + const packetLoss = + probe.packetLossPercent === undefined + ? undefined + : `Потери ${probe.packetLossPercent}%`; + const download = + probe.downloadMbps === undefined + ? undefined + : `↓ ${probe.downloadMbps.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} Мбит/с`; + + return [state, latency, packetLoss, download, probe.detail] + .filter(Boolean) + .join(" · "); +} diff --git a/extensions/ray-tools/src/tools/ping/provider.test.ts b/extensions/ray-tools/src/tools/ping/provider.test.ts new file mode 100644 index 00000000000..66c5b54e1ac --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/provider.test.ts @@ -0,0 +1,293 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + MacNetworkPingProvider, + getRouterHealthDetail, + normalizeRemoteEndpoint, + parseDefaultRoute, + parseDhcpRouter, + parseHardwarePortDevices, + parseNetworkQuality, + parsePingLatency, + parsePingStatistics, + parseVpnActivity, +} from "./provider"; + +describe("macOS network output parsers", () => { + it("parses a default gateway and interface defensively", () => { + expect( + parseDefaultRoute(` + route to: default + gateway: 192.168.1.1 + interface: en0 + `), + ).toEqual({ gateway: "192.168.1.1", interface: "en0" }); + + expect( + parseDefaultRoute("gateway: link#12\ninterface: en0"), + ).toBeUndefined(); + + expect( + parseHardwarePortDevices( + "Hardware Port: Wi-Fi\nDevice: en0\n\nHardware Port: Ethernet\nDevice: en1\n", + ), + ).toEqual(["en0", "en1"]); + expect( + parseDhcpRouter( + "router (ip_mult): {192.168.1.1}\nsubnet_mask (ip): 255.255.255.0", + ), + ).toBe("192.168.1.1"); + }); + + it("parses macOS ping latency and connected VPN services", () => { + expect( + parsePingLatency( + "64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.234 ms", + ), + ).toBe(1.234); + expect(parsePingLatency("request timeout")).toBeUndefined(); + + expect( + parseVpnActivity( + "Available network connection services in current set:\n* (Connected) Work VPN", + ), + ).toEqual({ active: true, serviceName: "Work VPN" }); + expect( + parseVpnActivity('* (Connected) 123 VPN (provider) "Sota Connect"'), + ).toEqual({ active: true, serviceName: "Sota Connect" }); + expect(parseVpnActivity("* (Disconnected) Work VPN")).toEqual({ + active: false, + }); + }); + + it("parses packet loss and average latency from macOS ping summaries", () => { + expect( + parsePingStatistics(` + 5 packets transmitted, 4 packets received, 20.0% packet loss + round-trip min/avg/max/stddev = 11.200/15.625/21.400/3.100 ms + `), + ).toEqual({ + packetsSent: 5, + packetsReceived: 4, + packetLossPercent: 20, + latencyMs: 15.625, + }); + expect( + getRouterHealthDetail({ packetLossPercent: 0, latencyMs: 12.5 }), + ).toBe("Роутер отвечает нормально."); + expect(getRouterHealthDetail({ packetLossPercent: 0, latencyMs: 80 })).toBe( + "Роутер отвечает медленно (задержка 80 мс).", + ); + expect(getRouterHealthDetail({ packetLossPercent: 20 })).toBe( + "Роутер теряет пакеты (20%).", + ); + }); + + it("parses download speed and measured interface from networkQuality JSON", () => { + expect( + parseNetworkQuality( + JSON.stringify({ dl_throughput: 190_092_752, interface_name: "utun6" }), + ), + ).toEqual({ downloadMbps: 190.1, interfaceName: "utun6" }); + expect(parseNetworkQuality("not JSON")).toBeUndefined(); + }); + + it("uses the safe default for malformed or credential-bearing endpoints", () => { + expect(normalizeRemoteEndpoint("not a URL")).toBe("https://example.com/"); + expect(normalizeRemoteEndpoint("http://example.com")).toBe( + "https://example.com/", + ); + expect(normalizeRemoteEndpoint("https://user:pass@example.com")).toBe( + "https://example.com/", + ); + expect(normalizeRemoteEndpoint("https://status.example.test/health")).toBe( + "https://status.example.test/health", + ); + }); +}); + +describe("MacNetworkPingProvider", () => { + it("finds and checks the physical router when the VPN owns the default route", async () => { + const commands: Array<{ command: string; args: readonly string[] }> = []; + const executor = async (command: string, args: readonly string[]) => { + commands.push({ command, args }); + if (command === "/sbin/route") { + return { + stdout: "route to: default\ngateway: 10.8.0.1\ninterface: utun6\n", + }; + } + if (command === "/usr/sbin/networksetup") { + return { stdout: "Hardware Port: Wi-Fi\nDevice: en0\n" }; + } + if (command === "/usr/sbin/ipconfig") { + return { stdout: "router (ip_mult): {192.168.1.1}\n" }; + } + if (command === "/sbin/ping") { + return { + stdout: + "5 packets transmitted, 5 packets received, 0.0% packet loss\n" + + "round-trip min/avg/max/stddev = 1.0/2.5/4.0/1.0 ms\n", + }; + } + return { stdout: "* (Disconnected) Work VPN\n" }; + }; + const fetcher: typeof fetch = async (input, init) => + new Response(null, { + status: String(input).includes("generate_204") ? 204 : 200, + headers: init?.headers, + }); + + const result = await new MacNetworkPingProvider({ + executor, + fetcher, + remoteEndpoint: "https://status.example.test/health", + internetEndpoint: "https://connectivity.test/generate_204", + }).check(); + + expect(result.gateway).toMatchObject({ + state: "pass", + target: "192.168.1.1", + detail: "Роутер отвечает нормально.", + }); + expect(commands).toEqual( + expect.arrayContaining([ + { command: "/usr/sbin/networksetup", args: ["-listallhardwareports"] }, + { command: "/usr/sbin/ipconfig", args: ["getpacket", "en0"] }, + ]), + ); + }); + + it("runs the local, internet, remote, and VPN layers without live network access", async () => { + const commands: Array<{ command: string; args: readonly string[] }> = []; + const requests: Array<{ url: string; signal?: AbortSignal }> = []; + const executor = async (command: string, args: readonly string[]) => { + commands.push({ command, args }); + if (command === "/sbin/route") { + return { + stdout: "gateway: 192.168.1.1\ninterface: en0\n", + }; + } + if (command === "/sbin/ping") { + return { + stdout: + "5 packets transmitted, 5 packets received, 0.0% packet loss\n" + + "round-trip min/avg/max/stddev = 1.0/2.5/4.0/1.0 ms\n", + }; + } + if (command === "/usr/bin/networkQuality") { + return { + stdout: JSON.stringify({ + dl_throughput: 84_300_000, + interface_name: "en0", + }), + }; + } + return { stdout: "* (Disconnected) Work VPN\n" }; + }; + const fetcher: typeof fetch = async (input, init) => { + requests.push({ url: String(input), signal: init?.signal ?? undefined }); + return new Response(null, { + status: String(input).includes("generate_204") ? 204 : 200, + }); + }; + + const result = await new MacNetworkPingProvider({ + executor, + fetcher, + now: () => 1_000, + remoteEndpoint: "https://status.example.test/health", + internetEndpoint: "https://connectivity.test/generate_204", + }).check(); + + expect(result.gateway).toMatchObject({ + state: "pass", + latencyMs: 2.5, + target: "192.168.1.1", + }); + expect(result.internet).toMatchObject({ + state: "pass", + target: "connectivity.test", + packetLossPercent: 0, + }); + expect(result.server).toMatchObject({ + state: "pass", + target: "status.example.test", + }); + expect(result.vpn.state).toBe("not-detected"); + expect(result.speed).toMatchObject({ state: "not-detected" }); + expect(commands.map(({ command }) => command)).toEqual( + expect.arrayContaining(["/sbin/route", "/sbin/ping", "/usr/sbin/scutil"]), + ); + expect( + commands.some(({ command }) => command === "/usr/bin/networkQuality"), + ).toBe(false); + expect(requests.map(({ url }) => url)).toEqual( + expect.arrayContaining([ + "https://connectivity.test/generate_204", + "https://status.example.test/health", + ]), + ); + expect(requests.every(({ signal }) => signal)).toBe(true); + }); + + it("runs the traffic-heavy download test only when explicitly requested", async () => { + const executor = async (command: string) => { + expect(command).toBe("/usr/bin/networkQuality"); + return { + stdout: JSON.stringify({ + dl_throughput: 84_300_000, + interface_name: "en0", + }), + }; + }; + + const result = await new MacNetworkPingProvider({ + executor, + }).measureSpeed(); + + expect(result).toMatchObject({ + state: "pass", + downloadMbps: 84.3, + target: "en0", + }); + }); + + it("marks HTTP failures and bounded timeouts instead of throwing", async () => { + vi.useFakeTimers(); + + try { + const executor = async (command: string) => { + if (command === "/sbin/route") { + return { stdout: "gateway: 192.168.1.1\ninterface: en0\n" }; + } + if (command === "/sbin/ping") { + return { stdout: "time=1 ms" }; + } + return { stdout: "" }; + }; + const fetcher: typeof fetch = async (_input, init) => + new Promise((_, reject) => { + init?.signal?.addEventListener( + "abort", + () => reject(new Error("aborted")), + { once: true }, + ); + }); + + const promise = new MacNetworkPingProvider({ executor, fetcher }).check(); + await vi.advanceTimersByTimeAsync(5_000); + const result = await promise; + + expect(result.internet).toMatchObject({ + state: "fail", + detail: "Время ожидания запроса истекло", + }); + expect(result.server).toMatchObject({ + state: "fail", + detail: "Время ожидания запроса истекло", + }); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/extensions/ray-tools/src/tools/ping/provider.ts b/extensions/ray-tools/src/tools/ping/provider.ts new file mode 100644 index 00000000000..a02920b7ece --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/provider.ts @@ -0,0 +1,620 @@ +import { execFile } from "node:child_process"; +import { isIP } from "node:net"; +import { promisify } from "node:util"; + +import type { PingProbeResult, PingProbeSet, PingProvider } from "./types"; + +export const INTERNET_ENDPOINT = + "https://connectivitycheck.gstatic.com/generate_204"; +export const DEFAULT_REMOTE_ENDPOINT = "https://example.com/"; + +const ROUTE_COMMAND = "/sbin/route"; +const PING_COMMAND = "/sbin/ping"; +const SCUTIL_COMMAND = "/usr/sbin/scutil"; +const NETWORKSETUP_COMMAND = "/usr/sbin/networksetup"; +const IPCONFIG_COMMAND = "/usr/sbin/ipconfig"; +const NETWORK_QUALITY_COMMAND = "/usr/bin/networkQuality"; +const ROUTE_TIMEOUT_MS = 2_000; +const PING_TIMEOUT_MS = 6_500; +const HTTP_TIMEOUT_MS = 5_000; +const NETWORK_QUALITY_TIMEOUT_MS = 12_000; +const MAX_COMMAND_OUTPUT_BYTES = 32 * 1024; +const ROUTER_SLOW_LATENCY_MS = 50; + +export interface DefaultRoute { + gateway: string; + interface?: string; +} + +export interface CommandResult { + stdout: string; + stderr?: string; +} + +export type CommandExecutor = ( + command: string, + args: readonly string[], + options: { timeout: number }, +) => Promise; + +export interface PingProviderOptions { + executor?: CommandExecutor; + fetcher?: typeof fetch; + now?: () => number; + internetEndpoint?: string; + remoteEndpoint?: string; +} + +const execFileAsync = promisify(execFile); + +const defaultExecutor: CommandExecutor = async (command, args, options) => { + const result = await execFileAsync(command, [...args], { + encoding: "utf8", + maxBuffer: MAX_COMMAND_OUTPUT_BYTES, + timeout: options.timeout, + }); + + return { + stdout: String(result.stdout), + stderr: String(result.stderr), + }; +}; + +function asValidGateway(value: string | undefined): string | undefined { + if ( + !value || + value.startsWith("link#") || + value === "-" || + value === "none" + ) { + return undefined; + } + + const withoutScope = value.replace(/%[^%]+$/u, ""); + return isIP(withoutScope) > 0 ? value : undefined; +} + +function asInterface(value: string | undefined): string | undefined { + return value && /^[A-Za-z0-9._-]+$/u.test(value) ? value : undefined; +} + +export function parseDefaultRoute(output: string): DefaultRoute | undefined { + const gateway = asValidGateway(output.match(/^\s*gateway:\s*(\S+)/imu)?.[1]); + + if (!gateway) { + return undefined; + } + + const networkInterface = asInterface( + output.match(/^\s*interface:\s*(\S+)/imu)?.[1], + ); + + return networkInterface + ? { gateway, interface: networkInterface } + : { gateway }; +} + +export function parseHardwarePortDevices(output: string): string[] { + return Array.from( + new Set( + Array.from( + output.matchAll(/^\s*Device:\s*(en\d+)\s*$/gimu), + (match) => match[1], + ), + ), + ); +} + +export function parseDhcpRouter(output: string): string | undefined { + const value = output.match( + /^\s*router\s+\([^)]*\):\s*(?:\{\s*)?([^\s,}]+)/imu, + )?.[1]; + return asValidGateway(value); +} + +export function parsePingLatency(output: string): number | undefined { + const value = output.match(/\btime=([0-9]+(?:\.[0-9]+)?)\s*ms\b/iu)?.[1]; + if (!value) { + return undefined; + } + + const latency = Number(value); + return Number.isFinite(latency) && latency >= 0 ? latency : undefined; +} + +export interface PingStatistics { + packetsSent?: number; + packetsReceived?: number; + packetLossPercent?: number; + latencyMs?: number; +} + +export function parsePingStatistics(output: string): PingStatistics { + const packetSummary = output.match( + /(\d+)\s+packets transmitted,\s*(\d+)\s+packets received,\s*([0-9]+(?:\.[0-9]+)?)%\s+packet loss/iu, + ); + const roundTripSummary = output.match( + /round-trip min\/avg\/max\/stddev\s*=\s*[0-9.]+\/([0-9.]+)\/[0-9.]+\/[0-9.]+\s*ms/iu, + ); + + return { + packetsSent: packetSummary ? Number(packetSummary[1]) : undefined, + packetsReceived: packetSummary ? Number(packetSummary[2]) : undefined, + packetLossPercent: packetSummary ? Number(packetSummary[3]) : undefined, + latencyMs: roundTripSummary ? Number(roundTripSummary[1]) : undefined, + }; +} + +export function getRouterHealthDetail(statistics: PingStatistics): string { + if ((statistics.packetLossPercent ?? 0) > 0) { + return `Роутер теряет пакеты (${statistics.packetLossPercent}%).`; + } + + if ( + statistics.latencyMs !== undefined && + statistics.latencyMs > ROUTER_SLOW_LATENCY_MS + ) { + return `Роутер отвечает медленно (задержка ${statistics.latencyMs.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс).`; + } + + return "Роутер отвечает нормально."; +} + +export interface NetworkQualityResult { + downloadMbps: number; + interfaceName?: string; +} + +export function parseNetworkQuality( + output: string, +): NetworkQualityResult | undefined { + try { + const value = JSON.parse(output) as { + dl_throughput?: unknown; + interface_name?: unknown; + }; + if ( + typeof value.dl_throughput !== "number" || + !Number.isFinite(value.dl_throughput) || + value.dl_throughput < 0 + ) { + return undefined; + } + + return { + downloadMbps: Math.round((value.dl_throughput / 1_000_000) * 10) / 10, + interfaceName: + typeof value.interface_name === "string" + ? asInterface(value.interface_name) + : undefined, + }; + } catch { + return undefined; + } +} + +export interface VpnActivity { + active: boolean; + serviceName?: string; +} + +export function parseVpnActivity(output: string): VpnActivity { + const connectedLine = output + .split(/\r?\n/u) + .find((line) => /\(\s*connected\s*\)/iu.test(line)); + + if (!connectedLine) { + return { active: false }; + } + + const quotedServiceName = connectedLine.match(/"([^"]+)"/u)?.[1]?.trim(); + const serviceName = + quotedServiceName ?? + connectedLine.replace(/^\s*\*?\s*\(\s*connected\s*\)\s*/iu, "").trim(); + + return serviceName ? { active: true, serviceName } : { active: true }; +} + +export function normalizeRemoteEndpoint(value: unknown): string { + if (typeof value !== "string" || !value.trim()) { + return DEFAULT_REMOTE_ENDPOINT; + } + + try { + const url = new URL(value.trim()); + if (url.protocol !== "https:" || url.username || url.password) { + return DEFAULT_REMOTE_ENDPOINT; + } + + return url.toString(); + } catch { + return DEFAULT_REMOTE_ENDPOINT; + } +} + +function endpointName(endpoint: string): string { + try { + return new URL(endpoint).host; + } catch { + return endpoint; + } +} + +function makeProbe( + id: PingProbeResult["id"], + label: string, + state: PingProbeResult["state"], + detail: string, + options: Pick< + PingProbeResult, + | "latencyMs" + | "target" + | "packetLossPercent" + | "packetsSent" + | "packetsReceived" + | "downloadMbps" + > = {}, +): PingProbeResult { + return { id, label, state, detail, ...options }; +} + +function errorLooksLikeTimeout(error: unknown): boolean { + if (!error || typeof error !== "object") { + return false; + } + + const candidate = error as { + code?: unknown; + killed?: unknown; + signal?: unknown; + }; + return ( + candidate.code === "ETIMEDOUT" || + candidate.killed === true || + candidate.signal === "SIGTERM" + ); +} + +export class MacNetworkPingProvider implements PingProvider { + private readonly executor: CommandExecutor; + private readonly fetcher: typeof fetch; + private readonly now: () => number; + private readonly internetEndpoint: string; + private readonly remoteEndpoint: string; + + constructor(options: PingProviderOptions = {}) { + this.executor = options.executor ?? defaultExecutor; + this.fetcher = options.fetcher ?? fetch; + this.now = options.now ?? Date.now; + this.internetEndpoint = options.internetEndpoint ?? INTERNET_ENDPOINT; + this.remoteEndpoint = normalizeRemoteEndpoint(options.remoteEndpoint); + } + + async check(): Promise { + const routeResult = await this.readDefaultRoute(); + const defaultRoute = routeResult.route; + const defaultRouteIsVpn = defaultRoute?.interface + ? /^utun\d+$/iu.test(defaultRoute.interface) + : false; + const gatewayRoute = + defaultRoute && !defaultRouteIsVpn + ? defaultRoute + : ((await this.readPhysicalRoute()) ?? defaultRoute); + const gateway = await this.probeGateway(gatewayRoute, routeResult.reason); + + const [internetHttp, internetPing, server, vpn] = await Promise.all([ + this.probeHttp( + "internet", + "Проверка интернета", + this.internetEndpoint, + 204, + ), + this.probePing(endpointName(this.internetEndpoint)), + this.probeHttp( + "server", + `Удалённый сервер (${endpointName(this.remoteEndpoint)})`, + this.remoteEndpoint, + ), + this.probeVpn(routeResult.route), + ]); + + const internet = { ...internetHttp, ...internetPing }; + + return { + gateway, + internet, + server, + vpn, + speed: makeProbe( + "speed", + "Скорость скачивания", + "not-detected", + "Нажмите «Измерить скорость скачивания», чтобы узнать результат.", + ), + }; + } + + private async readDefaultRoute(): Promise<{ + route?: DefaultRoute; + reason: "command-failed" | "not-found"; + }> { + try { + const result = await this.executor( + ROUTE_COMMAND, + ["-n", "get", "default"], + { timeout: ROUTE_TIMEOUT_MS }, + ); + const route = parseDefaultRoute(result.stdout); + return route ? { route, reason: "not-found" } : { reason: "not-found" }; + } catch { + return { reason: "command-failed" }; + } + } + + private async readPhysicalRoute(): Promise { + let hardwarePorts: string; + try { + const result = await this.executor( + NETWORKSETUP_COMMAND, + ["-listallhardwareports"], + { timeout: ROUTE_TIMEOUT_MS }, + ); + hardwarePorts = result.stdout; + } catch { + return undefined; + } + + const devices = parseHardwarePortDevices(hardwarePorts); + const routes = await Promise.all( + devices.map(async (networkInterface) => { + try { + const result = await this.executor( + IPCONFIG_COMMAND, + ["getpacket", networkInterface], + { timeout: ROUTE_TIMEOUT_MS }, + ); + const gateway = parseDhcpRouter(result.stdout); + return gateway ? { gateway, interface: networkInterface } : undefined; + } catch { + return undefined; + } + }), + ); + + return routes.find((route) => route !== undefined); + } + + private async probeGateway( + route: DefaultRoute | undefined, + reason: "command-failed" | "not-found", + ): Promise { + if (!route) { + return makeProbe( + "gateway", + "Роутер", + "unknown", + reason === "command-failed" + ? "Не удалось определить роутер. Проверьте подключение или VPN." + : "Роутер не найден. В VPN-режиме это может быть нормально.", + ); + } + + const routeDescription = route.interface + ? `Роутер (${route.interface})` + : "Роутер"; + + try { + const result = await this.executor( + PING_COMMAND, + [ + "-n", + ...(route.interface ? ["-b", route.interface] : []), + "-c", + "5", + "-W", + "1000", + route.gateway, + ], + { timeout: PING_TIMEOUT_MS + 500 }, + ); + const statistics = parsePingStatistics(result.stdout); + + return makeProbe( + "gateway", + "Роутер", + "pass", + getRouterHealthDetail(statistics), + { + ...statistics, + latencyMs: statistics.latencyMs ?? parsePingLatency(result.stdout), + target: route.gateway, + }, + ); + } catch (error) { + return makeProbe( + "gateway", + "Роутер", + "fail", + `${routeDescription} не отвечает${errorLooksLikeTimeout(error) ? " до истечения тайм-аута" : ""}`, + { target: route.gateway }, + ); + } + } + + private async probePing( + target: string, + ): Promise< + Pick< + PingProbeResult, + "latencyMs" | "packetLossPercent" | "packetsSent" | "packetsReceived" + > + > { + try { + const result = await this.executor( + PING_COMMAND, + ["-n", "-c", "5", "-W", "1000", target], + { timeout: PING_TIMEOUT_MS + 500 }, + ); + return parsePingStatistics(result.stdout); + } catch { + return {}; + } + } + + async measureSpeed(): Promise { + try { + const result = await this.executor( + NETWORK_QUALITY_COMMAND, + ["-c", "-u", "-M", "8"], + { timeout: NETWORK_QUALITY_TIMEOUT_MS }, + ); + const quality = parseNetworkQuality(result.stdout); + if (!quality) { + return makeProbe( + "speed", + "Скорость скачивания", + "unknown", + "Не удалось получить результат измерения скорости.", + ); + } + + return makeProbe( + "speed", + "Скорость скачивания", + "pass", + "Скорость измерена", + { + downloadMbps: quality.downloadMbps, + target: quality.interfaceName, + }, + ); + } catch { + return makeProbe( + "speed", + "Скорость скачивания", + "unknown", + "Не удалось измерить скорость скачивания.", + ); + } + } + + private async probeHttp( + id: "internet" | "server", + label: string, + endpoint: string, + expectedStatus?: number, + ): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS); + const startedAt = this.now(); + const target = endpointName(endpoint); + + try { + let response: Response; + try { + response = await this.fetcher(endpoint, { + headers: { Accept: "*/*" }, + method: "GET", + signal: controller.signal, + }); + } catch { + return makeProbe( + id, + label, + "fail", + controller.signal.aborted + ? "Время ожидания запроса истекло" + : "Запрос не дал HTTP-ответа", + { target }, + ); + } + + const latencyMs = Math.max(0, this.now() - startedAt); + const statusIsExpected = + expectedStatus === undefined + ? response.ok + : response.status === expectedStatus; + + if (!statusIsExpected) { + if (response.body) { + await response.body.cancel().catch(() => undefined); + } + + const expected = + expectedStatus === undefined + ? "успешный HTTP-статус" + : `HTTP ${expectedStatus}`; + return makeProbe( + id, + label, + "fail", + `Ожидался ${expected}, получен HTTP ${response.status}`, + { latencyMs, target }, + ); + } + + if (response.body) { + await response.body.cancel().catch(() => undefined); + } + + return makeProbe( + id, + label, + "pass", + `HTTP ${response.status}, ${target}`, + { latencyMs, target }, + ); + } finally { + clearTimeout(timeout); + } + } + + private async probeVpn( + route: DefaultRoute | undefined, + ): Promise { + if (route?.interface && /^utun\d+$/iu.test(route.interface)) { + return makeProbe( + "vpn", + "Активность VPN", + "pass", + `VPN подключён (${route.interface})`, + { target: route.interface }, + ); + } + + let output: string; + try { + const result = await this.executor(SCUTIL_COMMAND, ["--nc", "list"], { + timeout: ROUTE_TIMEOUT_MS, + }); + output = result.stdout; + } catch { + return makeProbe( + "vpn", + "Активность VPN", + "unknown", + "Не удалось проверить состояние VPN в macOS.", + ); + } + + const activity = parseVpnActivity(output); + if (activity.active) { + return makeProbe( + "vpn", + "Активность VPN", + "pass", + activity.serviceName + ? `VPN подключён: ${activity.serviceName}` + : "Обнаружено активное VPN-подключение", + { target: activity.serviceName }, + ); + } + + return makeProbe( + "vpn", + "Активность VPN", + "not-detected", + "Активное VPN-подключение не обнаружено (проверка приблизительная).", + ); + } +} diff --git a/extensions/ray-tools/src/tools/ping/service.test.ts b/extensions/ray-tools/src/tools/ping/service.test.ts new file mode 100644 index 00000000000..1fc5b8bbeda --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/service.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { PingService } from "./service"; +import type { PingProbeSet, PingProvider } from "./types"; + +const probes: PingProbeSet = { + gateway: { + id: "gateway", + label: "Роутер", + state: "pass", + detail: "Gateway replied", + }, + internet: { + id: "internet", + label: "Проверка интернета", + state: "pass", + detail: "HTTP 204", + latencyMs: 12, + }, + server: { + id: "server", + label: "Удалённый сервер", + state: "pass", + detail: "HTTP 200", + latencyMs: 20, + }, + vpn: { + id: "vpn", + label: "Активность VPN", + state: "not-detected", + detail: "No active VPN detected", + }, + speed: { + id: "speed", + label: "Скорость скачивания", + state: "pass", + detail: "Measured on en0", + downloadMbps: 84.3, + }, +}; + +describe("PingService", () => { + it("adds a deterministic timestamp and diagnosis to provider probes", async () => { + const provider: PingProvider = { check: async () => probes }; + const result = await new PingService( + provider, + () => new Date("2026-08-11T00:00:00.000Z"), + ).check(); + + expect(result).toEqual({ + ...probes, + checkedAt: "2026-08-11T00:00:00.000Z", + diagnosis: expect.objectContaining({ code: "healthy" }), + }); + }); +}); diff --git a/extensions/ray-tools/src/tools/ping/service.ts b/extensions/ray-tools/src/tools/ping/service.ts new file mode 100644 index 00000000000..d1df6e2a88f --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/service.ts @@ -0,0 +1,19 @@ +import { diagnosePing } from "./domain"; +import type { PingProvider, PingResult } from "./types"; + +export class PingService { + constructor( + private readonly provider: PingProvider, + private readonly clock: () => Date = () => new Date(), + ) {} + + async check(): Promise { + const probes = await this.provider.check(); + + return { + ...probes, + checkedAt: this.clock().toISOString(), + diagnosis: diagnosePing(probes), + }; + } +} diff --git a/extensions/ray-tools/src/tools/ping/types.ts b/extensions/ray-tools/src/tools/ping/types.ts new file mode 100644 index 00000000000..0c723262f2f --- /dev/null +++ b/extensions/ray-tools/src/tools/ping/types.ts @@ -0,0 +1,47 @@ +export type PingProbeId = "gateway" | "internet" | "server" | "vpn" | "speed"; + +export type ProbeState = "pass" | "fail" | "unknown" | "not-detected"; + +export interface PingProbeResult { + id: PingProbeId; + label: string; + state: ProbeState; + detail: string; + target?: string; + latencyMs?: number; + packetLossPercent?: number; + packetsSent?: number; + packetsReceived?: number; + downloadMbps?: number; +} + +export interface PingProbeSet { + gateway: PingProbeResult; + internet: PingProbeResult; + server: PingProbeResult; + vpn: PingProbeResult; + speed: PingProbeResult; +} + +export type PingDiagnosisCode = + | "healthy" + | "local-network" + | "isp-or-internet" + | "remote-server" + | "vpn" + | "inconclusive"; + +export interface PingDiagnosis { + code: PingDiagnosisCode; + title: string; + summary: string; +} + +export interface PingResult extends PingProbeSet { + checkedAt: string; + diagnosis: PingDiagnosis; +} + +export interface PingProvider { + check(): Promise; +} diff --git a/extensions/ray-tools/src/tools/proofread-russian/domain.test.ts b/extensions/ray-tools/src/tools/proofread-russian/domain.test.ts new file mode 100644 index 00000000000..24f5ffde4cb --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/domain.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { applyCorrections, findRussianContextIssues } from "./domain"; + +describe("Russian proofreading corrections", () => { + it("applies multiple suggested corrections without shifting later offsets", () => { + const text = "Это тект и очен хорош."; + const typoOffset = text.indexOf("тект"); + const adverbOffset = text.indexOf("очен"); + + expect( + applyCorrections(text, [ + { + message: "Исправьте слово", + replacements: ["текст"], + offset: typoOffset, + length: "тект".length, + category: "spelling", + }, + { + message: "Исправьте слово", + replacements: ["очень"], + offset: adverbOffset, + length: "очен".length, + category: "spelling", + }, + ]), + ).toBe("Это текст и очень хорош."); + }); + + it("suggests the joined spelling of неинтересно without a contrast", () => { + const text = "Мне не интересно"; + + expect(findRussianContextIssues(text)).toEqual([ + expect.objectContaining({ + category: "spelling", + replacements: ["неинтересно"], + offset: text.indexOf("не интересно"), + length: "не интересно".length, + }), + ]); + }); + + it("keeps separate не интересно with explicit negation or contrast", () => { + expect(findRussianContextIssues("Мне совсем не интересно")).toEqual([]); + expect(findRussianContextIssues("Мне это не интересно, а важно")).toEqual( + [], + ); + }); +}); diff --git a/extensions/ray-tools/src/tools/proofread-russian/domain.ts b/extensions/ray-tools/src/tools/proofread-russian/domain.ts new file mode 100644 index 00000000000..a5e5098656f --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/domain.ts @@ -0,0 +1,103 @@ +import type { ProofreadingIssue } from "./types"; + +const SEPARATE_NE_INTERESTNO_PATTERN = + /(? { + const matchedText = match[0]; + const offset = match.index ?? -1; + + if ( + offset < 0 || + EXPLICIT_NEGATION_PREFIX.test(text.slice(0, offset)) || + CONTRAST_OR_QUESTION_SUFFIX.test(text.slice(offset + matchedText.length)) + ) { + return []; + } + + return [ + { + message: + "В значении «скучно» слово «неинтересно» обычно пишется слитно. Раздельное написание нужно при явном отрицании или противопоставлении.", + shortMessage: "Слитное написание «не»", + replacements: [matchedText.replace(/\s+/gu, "")], + offset, + length: matchedText.length, + category: "spelling", + ruleId: "RU_NE_INTERESTNO", + }, + ]; + }); +} + +export function mergeRussianContextIssues( + text: string, + issues: ProofreadingIssue[], +): ProofreadingIssue[] { + const contextIssues = findRussianContextIssues(text).filter( + (contextIssue) => !issues.some((issue) => overlaps(issue, contextIssue)), + ); + + return [...issues, ...contextIssues].sort( + (left, right) => left.offset - right.offset, + ); +} + +function isValidIssue(text: string, issue: ProofreadingIssue): boolean { + return ( + Number.isInteger(issue.offset) && + Number.isInteger(issue.length) && + issue.offset >= 0 && + issue.length >= 0 && + issue.offset + issue.length <= text.length && + typeof issue.replacements[0] === "string" + ); +} + +export function applyCorrections( + text: string, + issues: ProofreadingIssue[], +): string { + const candidates = issues + .map((issue, index) => ({ issue, index })) + .filter(({ issue }) => isValidIssue(text, issue)) + .sort( + (left, right) => + left.issue.offset - right.issue.offset || + right.issue.length - left.issue.length || + left.index - right.index, + ); + + const selected: ProofreadingIssue[] = []; + let coveredUntil = -1; + + for (const { issue } of candidates) { + if (issue.offset < coveredUntil) { + continue; + } + + selected.push(issue); + coveredUntil = issue.offset + issue.length; + } + + return selected + .sort((left, right) => right.offset - left.offset) + .reduce( + (correctedText, issue) => + correctedText.slice(0, issue.offset) + + issue.replacements[0] + + correctedText.slice(issue.offset + issue.length), + text, + ); +} diff --git a/extensions/ray-tools/src/tools/proofread-russian/markdown.test.ts b/extensions/ray-tools/src/tools/proofread-russian/markdown.test.ts new file mode 100644 index 00000000000..7c46c06f01c --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/markdown.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; + +import { + asMarkdownCodeBlock, + formatIssueDetail, + formatIssueTitle, + formatResult, +} from "./markdown"; +import type { ProofreadingResult } from "./types"; + +describe("Russian proofreading result markdown", () => { + it("shows the corrected text and each suggested replacement", () => { + const result: ProofreadingResult = { + text: "Это тект где нужна запятая.", + correctedText: "Это текст, где нужна запятая.", + language: "ru-RU", + provider: "languagetool", + issues: [ + { + message: "Возможно найдена орфографическая ошибка.", + shortMessage: "Орфографическая ошибка", + replacements: ["текст"], + offset: 4, + length: 4, + category: "spelling", + ruleId: "MORFOLOGIK_RULE_RU_RU", + }, + ], + }; + + const markdown = formatResult(result); + + expect(markdown).toContain("## 1 issue found"); + expect(markdown).toContain(asMarkdownCodeBlock(result.correctedText)); + expect(markdown).toContain("#### 1. Spelling"); + expect(markdown).toContain(asMarkdownCodeBlock("тект")); + expect(markdown).toContain(asMarkdownCodeBlock("текст")); + expect(markdown).toContain("Возможно найдена орфографическая ошибка."); + }); + + it("reports when the text has no spelling or punctuation issues", () => { + const result: ProofreadingResult = { + text: "Это правильный текст.", + correctedText: "Это правильный текст.", + language: "ru-RU", + provider: "languagetool", + issues: [], + }; + + expect(formatResult(result)).toContain("## No issues found"); + }); + + it("formats every issue as a separate list item", () => { + const text = "Это тект где нужна запятая."; + const result: ProofreadingResult = { + text, + correctedText: "Это текст, где нужна запятая.", + language: "ru-RU", + provider: "languagetool", + issues: [ + { + message: "Возможно найдена орфографическая ошибка.", + replacements: ["текст"], + offset: text.indexOf("тект"), + length: "тект".length, + category: "spelling", + }, + { + message: "Пропущена запятая.", + replacements: ["текст, где"], + offset: text.indexOf("тект где"), + length: "тект где".length, + category: "punctuation", + }, + ], + }; + + expect( + result.issues.map((issue) => formatIssueTitle(result, issue)), + ).toEqual(["тект → текст", "тект где → текст, где"]); + expect(formatIssueDetail(result, result.issues[1]!, 1)).toContain( + "#### 2. Punctuation", + ); + }); +}); diff --git a/extensions/ray-tools/src/tools/proofread-russian/markdown.ts b/extensions/ray-tools/src/tools/proofread-russian/markdown.ts new file mode 100644 index 00000000000..651ef053a3e --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/markdown.ts @@ -0,0 +1,124 @@ +import type { ProofreadingIssue, ProofreadingResult } from "./types"; + +const CATEGORY_LABELS: Record = { + spelling: "Spelling", + punctuation: "Punctuation", + grammar: "Grammar", + style: "Style", + other: "Other", +}; + +export function getIssueCategoryLabel( + category: ProofreadingIssue["category"], +): string { + return CATEGORY_LABELS[category]; +} + +function longestBacktickRun(text: string): number { + let longest = 0; + let current = 0; + + for (const character of text) { + if (character === "`") { + current += 1; + continue; + } + + longest = Math.max(longest, current); + current = 0; + } + + return Math.max(longest, current); +} + +export function asMarkdownCodeBlock(text: string): string { + const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1)); + return `${fence}\n${text}\n${fence}`; +} + +export function getIssueExcerpt( + result: ProofreadingResult, + issue: ProofreadingIssue, +): string { + const excerpt = result.text.slice(issue.offset, issue.offset + issue.length); + return excerpt || "(no text)"; +} + +function asSingleLine(text: string): string { + return text.replace(/\s+/gu, " ").trim() || "(no text)"; +} + +export function formatIssueTitle( + result: ProofreadingResult, + issue: ProofreadingIssue, +): string { + const replacement = issue.replacements[0] ?? "(no automatic suggestion)"; + return `${asSingleLine(getIssueExcerpt(result, issue))} → ${asSingleLine(replacement)}`; +} + +export function formatIssueDetail( + result: ProofreadingResult, + issue: ProofreadingIssue, + index: number, +): string { + const replacement = issue.replacements[0] ?? "(no automatic suggestion)"; + + return [ + `#### ${index + 1}. ${CATEGORY_LABELS[issue.category]}`, + "", + "**Found**", + "", + asMarkdownCodeBlock(getIssueExcerpt(result, issue)), + "", + "**Suggested**", + "", + asMarkdownCodeBlock(replacement), + "", + "**Why**", + "", + asMarkdownCodeBlock(issue.message), + ].join("\n"); +} + +export function formatCorrectedText(result: ProofreadingResult): string { + return [ + "## Corrected text", + "", + asMarkdownCodeBlock(result.correctedText), + "", + "### Original text", + "", + asMarkdownCodeBlock(result.text), + ].join("\n"); +} + +export function formatResult(result: ProofreadingResult): string { + const issueCount = result.issues.length; + const heading = + issueCount === 0 + ? "## No issues found" + : `## ${issueCount} ${issueCount === 1 ? "issue" : "issues"} found`; + const sections = [ + heading, + "", + "### Corrected text", + "", + asMarkdownCodeBlock(result.correctedText), + ]; + + if (issueCount > 0) { + sections.push( + "", + "### Suggestions", + "", + ...result.issues.map((issue, index) => + formatIssueDetail(result, issue, index), + ), + ); + } else { + sections.push("", "Russian spelling and punctuation look good."); + } + + sections.push("", "### Original text", "", asMarkdownCodeBlock(result.text)); + return sections.join("\n"); +} diff --git a/extensions/ray-tools/src/tools/proofread-russian/provider.test.ts b/extensions/ray-tools/src/tools/proofread-russian/provider.test.ts new file mode 100644 index 00000000000..872c60e784b --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/provider.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; + +import { LanguageToolProvider, parseLanguageToolResponse } from "./provider"; + +describe("LanguageTool response", () => { + it("maps Russian spelling and punctuation matches to app issues", () => { + expect( + parseLanguageToolResponse({ + matches: [ + { + message: "Возможно найдена орфографическая ошибка.", + shortMessage: "Орфографическая ошибка", + replacements: [{ value: "текст" }], + offset: 13, + length: 4, + rule: { + id: "MORFOLOGIK_RULE_RU_RU", + category: { id: "TYPOS" }, + }, + }, + { + message: "Пропущена запятая перед союзом.", + shortMessage: "Пропущена запятая", + replacements: [{ value: "ошибкой, где" }], + offset: 20, + length: 11, + rule: { + id: "GDE_COMMA", + category: { id: "PUNCTUATION" }, + }, + }, + ], + }), + ).toEqual([ + { + message: "Возможно найдена орфографическая ошибка.", + shortMessage: "Орфографическая ошибка", + replacements: ["текст"], + offset: 13, + length: 4, + category: "spelling", + ruleId: "MORFOLOGIK_RULE_RU_RU", + }, + { + message: "Пропущена запятая перед союзом.", + shortMessage: "Пропущена запятая", + replacements: ["ошибкой, где"], + offset: 20, + length: 11, + category: "punctuation", + ruleId: "GDE_COMMA", + }, + ]); + }); +}); + +describe("LanguageTool provider", () => { + it("checks Russian text and builds the corrected result", async () => { + const requests: Array<{ url: string; init?: RequestInit }> = []; + const text = "Это тект."; + const fetcher: typeof fetch = async (input, init) => { + requests.push({ url: String(input), init }); + return new Response( + JSON.stringify({ + matches: [ + { + message: "Исправьте слово.", + replacements: [{ value: "текст" }], + offset: text.indexOf("тект"), + length: "тект".length, + rule: { + category: { id: "TYPOS" }, + }, + }, + ], + }), + ); + }; + + const result = await new LanguageToolProvider( + fetcher, + "https://example.test/check", + ).check(text); + + expect(result.correctedText).toBe("Это текст."); + expect(result.issues).toHaveLength(1); + expect(requests[0]?.url).toBe("https://example.test/check"); + expect(requests[0]?.init?.method).toBe("POST"); + + const body = new URLSearchParams(String(requests[0]?.init?.body)); + expect(body.get("text")).toBe(text); + expect(body.get("language")).toBe("ru-RU"); + expect(body.get("enabledOnly")).toBe("false"); + }); + + it("applies a punctuation suggestion", async () => { + const text = "Это текст где нужна запятая."; + const fetcher: typeof fetch = async () => + new Response( + JSON.stringify({ + matches: [ + { + message: "Пропущена запятая.", + replacements: [{ value: "текст, где" }], + offset: text.indexOf("текст где"), + length: "текст где".length, + rule: { + category: { id: "PUNCTUATION" }, + }, + }, + ], + }), + ); + + const result = await new LanguageToolProvider(fetcher).check(text); + + expect(result.correctedText).toBe("Это текст, где нужна запятая."); + expect(result.issues[0]?.category).toBe("punctuation"); + }); + + it("adds a contextual spelling suggestion for separate не интересно", async () => { + const text = "Мне не интересно"; + const fetcher: typeof fetch = async () => + new Response(JSON.stringify({ matches: [] })); + + const result = await new LanguageToolProvider(fetcher).check(text); + + expect(result.issues).toEqual([ + expect.objectContaining({ + category: "spelling", + replacements: ["неинтересно"], + offset: text.indexOf("не интересно"), + length: "не интересно".length, + ruleId: "RU_NE_INTERESTNO", + }), + ]); + expect(result.correctedText).toBe("Мне неинтересно"); + }); +}); diff --git a/extensions/ray-tools/src/tools/proofread-russian/provider.ts b/extensions/ray-tools/src/tools/proofread-russian/provider.ts new file mode 100644 index 00000000000..78ed196f648 --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/provider.ts @@ -0,0 +1,174 @@ +import { applyCorrections, mergeRussianContextIssues } from "./domain"; +import type { + ProofreadingIssue, + ProofreadingIssueCategory, + ProofreadingProvider, + ProofreadingResult, +} from "./types"; + +const LANGUAGE_TOOL_ENDPOINT = "https://api.languagetool.org/v2/check"; +const PROOFREADING_TIMEOUT_MS = 15_000; + +type RecordValue = Record; + +function asRecord(value: unknown): RecordValue | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as RecordValue) + : undefined; +} + +function asString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function asInteger(value: unknown): number | undefined { + return typeof value === "number" && Number.isInteger(value) && value >= 0 + ? value + : undefined; +} + +function getIssueCategory( + categoryId: unknown, + issueType: unknown, +): ProofreadingIssueCategory { + const category = asString(categoryId)?.toUpperCase() ?? ""; + const type = asString(issueType)?.toLowerCase() ?? ""; + + if (category.includes("PUNCTUATION") || type.includes("punctuation")) { + return "punctuation"; + } + + if ( + category.includes("TYPO") || + category.includes("SPELL") || + type.includes("spell") || + type.includes("misspell") + ) { + return "spelling"; + } + + if (category.includes("STYLE") || type.includes("style")) { + return "style"; + } + + if (category.includes("GRAMMAR") || type.includes("grammar")) { + return "grammar"; + } + + return "other"; +} + +export function parseLanguageToolResponse( + payload: unknown, +): ProofreadingIssue[] { + const response = asRecord(payload); + if (!response || !Array.isArray(response.matches)) { + throw new Error("Proofreading service returned an invalid response"); + } + + return response.matches.flatMap((rawMatch): ProofreadingIssue[] => { + const match = asRecord(rawMatch); + const message = asString(match?.message); + const offset = asInteger(match?.offset); + const length = asInteger(match?.length); + + if (!message || offset === undefined || length === undefined) { + return []; + } + + const replacements = Array.isArray(match?.replacements) + ? match.replacements.flatMap((replacement) => { + const value = asString(asRecord(replacement)?.value); + return value === undefined ? [] : [value]; + }) + : []; + const rule = asRecord(match?.rule); + const category = asRecord(rule?.category); + const shortMessage = asString(match?.shortMessage); + const ruleId = asString(rule?.id); + + return [ + { + message, + ...(shortMessage ? { shortMessage } : {}), + replacements, + offset, + length, + category: getIssueCategory(category?.id, rule?.issueType), + ...(ruleId ? { ruleId } : {}), + }, + ]; + }); +} + +export class LanguageToolProvider implements ProofreadingProvider { + constructor( + private readonly fetcher: typeof fetch = fetch, + private readonly endpoint = LANGUAGE_TOOL_ENDPOINT, + ) {} + + async check(text: string): Promise { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + PROOFREADING_TIMEOUT_MS, + ); + const body = new URLSearchParams({ + text, + language: "ru-RU", + enabledOnly: "false", + }); + + try { + let response: Response; + try { + response = await this.fetcher(this.endpoint, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + signal: controller.signal, + }); + } catch { + if (controller.signal.aborted) { + throw new Error("Proofreading service request timed out"); + } + + throw new Error("Unable to reach proofreading service"); + } + + if (!response.ok) { + throw new Error( + `Proofreading service returned HTTP ${response.status}`, + ); + } + + let payload: unknown; + try { + payload = await response.json(); + } catch { + if (controller.signal.aborted) { + throw new Error("Proofreading service request timed out"); + } + + throw new Error("Proofreading service returned invalid JSON"); + } + + const issues = mergeRussianContextIssues( + text, + parseLanguageToolResponse(payload), + ); + return { + text, + correctedText: applyCorrections(text, issues), + issues, + language: "ru-RU", + provider: "languagetool", + }; + } finally { + clearTimeout(timeout); + } + } +} diff --git a/extensions/ray-tools/src/tools/proofread-russian/service.test.ts b/extensions/ray-tools/src/tools/proofread-russian/service.test.ts new file mode 100644 index 00000000000..9f0c2dd3b3c --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/service.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +import { RussianProofreadingService } from "./service"; +import type { ProofreadingProvider } from "./types"; + +describe("RussianProofreadingService", () => { + it("rejects blank input before calling a provider", async () => { + let calls = 0; + const provider: ProofreadingProvider = { + check: async () => { + calls += 1; + throw new Error("must not be called"); + }, + }; + + await expect( + new RussianProofreadingService(provider).check(" \n "), + ).rejects.toThrow("Enter Russian text to check"); + expect(calls).toBe(0); + }); + + it("trims the text before sending it to the provider", async () => { + let receivedText = ""; + const provider: ProofreadingProvider = { + check: async (text) => { + receivedText = text; + return { + text, + correctedText: text, + issues: [], + language: "ru-RU", + provider: "test", + }; + }, + }; + + const result = await new RussianProofreadingService(provider).check( + " Привет, мир. ", + ); + + expect(receivedText).toBe("Привет, мир."); + expect(result.text).toBe("Привет, мир."); + }); +}); diff --git a/extensions/ray-tools/src/tools/proofread-russian/service.ts b/extensions/ray-tools/src/tools/proofread-russian/service.ts new file mode 100644 index 00000000000..3dfee855586 --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/service.ts @@ -0,0 +1,14 @@ +import type { ProofreadingProvider, ProofreadingResult } from "./types"; + +export class RussianProofreadingService { + constructor(private readonly provider: ProofreadingProvider) {} + + async check(text: string): Promise { + const normalizedText = text.trim(); + if (!normalizedText) { + throw new Error("Enter Russian text to check"); + } + + return this.provider.check(normalizedText); + } +} diff --git a/extensions/ray-tools/src/tools/proofread-russian/types.ts b/extensions/ray-tools/src/tools/proofread-russian/types.ts new file mode 100644 index 00000000000..dab3538f9b5 --- /dev/null +++ b/extensions/ray-tools/src/tools/proofread-russian/types.ts @@ -0,0 +1,24 @@ +export type ProofreadingIssueCategory = + "spelling" | "punctuation" | "grammar" | "style" | "other"; + +export interface ProofreadingIssue { + message: string; + shortMessage?: string; + replacements: string[]; + offset: number; + length: number; + category: ProofreadingIssueCategory; + ruleId?: string; +} + +export interface ProofreadingResult { + text: string; + correctedText: string; + issues: ProofreadingIssue[]; + language: "ru-RU"; + provider: string; +} + +export interface ProofreadingProvider { + check(text: string): Promise; +} diff --git a/extensions/ray-tools/src/tools/translate/domain.test.ts b/extensions/ray-tools/src/tools/translate/domain.test.ts new file mode 100644 index 00000000000..a25acc1c015 --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/domain.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { detectLanguage, getTargetLanguage } from "./domain"; + +describe("translation language direction", () => { + it("detects Russian text from Cyrillic letters", () => { + expect(detectLanguage("Привет, как дела?")).toBe("ru"); + }); + + it("detects English text from Latin letters", () => { + expect(detectLanguage("Hello, how are you?")).toBe("en"); + }); + + it("uses the first letter to break mixed-language ties", () => { + expect(detectLanguage("БA")).toBe("ru"); + expect(detectLanguage("AБ")).toBe("en"); + }); + + it("defaults to English when there are no supported letters", () => { + expect(detectLanguage("123 !?")).toBe("en"); + }); + + it("switches Russian to English and English to Russian", () => { + expect(getTargetLanguage("ru")).toBe("en"); + expect(getTargetLanguage("en")).toBe("ru"); + }); +}); diff --git a/extensions/ray-tools/src/tools/translate/domain.ts b/extensions/ray-tools/src/tools/translate/domain.ts new file mode 100644 index 00000000000..09ca5559aa1 --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/domain.ts @@ -0,0 +1,27 @@ +export type SupportedLanguage = "ru" | "en"; + +const CYRILLIC_LETTER = /[А-Яа-яЁё]/; +const LATIN_LETTER = /[A-Za-z]/; + +export function detectLanguage(text: string): SupportedLanguage { + const cyrillicLetters = text.match(/[А-Яа-яЁё]/g)?.length ?? 0; + const latinLetters = text.match(/[A-Za-z]/g)?.length ?? 0; + + if (cyrillicLetters === latinLetters) { + const firstCyrillicLetter = text.search(CYRILLIC_LETTER); + const firstLatinLetter = text.search(LATIN_LETTER); + + return firstCyrillicLetter !== -1 && + (firstLatinLetter === -1 || firstCyrillicLetter < firstLatinLetter) + ? "ru" + : "en"; + } + + return cyrillicLetters > latinLetters ? "ru" : "en"; +} + +export function getTargetLanguage( + sourceLanguage: SupportedLanguage, +): SupportedLanguage { + return sourceLanguage === "ru" ? "en" : "ru"; +} diff --git a/extensions/ray-tools/src/tools/translate/markdown.test.ts b/extensions/ray-tools/src/tools/translate/markdown.test.ts new file mode 100644 index 00000000000..bc8a6640aa5 --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/markdown.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { asMarkdownCodeBlock, formatResult } from "./markdown"; + +describe("translation result markdown", () => { + it("keeps user-controlled text inside code blocks", () => { + const result = formatResult( + { + text: "# translated\n[open link](https://example.com)\n```\nraw", + sourceLanguage: "en", + targetLanguage: "ru", + provider: "google-web", + }, + "Source\n![image](https://example.com/image.png)\n```\nraw", + ); + + expect(result).toContain( + "````\n# translated\n[open link](https://example.com)\n```\nraw\n````", + ); + expect(result).toContain( + "````\nSource\n![image](https://example.com/image.png)\n```\nraw\n````", + ); + }); + + it("chooses a fence longer than any backtick run in the text", () => { + const text = "````\ncontent\n````"; + + expect(asMarkdownCodeBlock(text)).toBe("`````\n" + text + "\n`````"); + }); +}); diff --git a/extensions/ray-tools/src/tools/translate/markdown.ts b/extensions/ray-tools/src/tools/translate/markdown.ts new file mode 100644 index 00000000000..b44ac30b4dd --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/markdown.ts @@ -0,0 +1,42 @@ +import type { TranslationResult } from "./types"; + +function longestBacktickRun(text: string): number { + let longest = 0; + let current = 0; + + for (const character of text) { + if (character === "`") { + current += 1; + continue; + } + + longest = Math.max(longest, current); + current = 0; + } + + return Math.max(longest, current); +} + +export function asMarkdownCodeBlock(text: string): string { + const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1)); + return `${fence}\n${text}\n${fence}`; +} + +export function formatResult( + result: TranslationResult, + sourceText: string, +): string { + const direction = `${result.sourceLanguage.toUpperCase()} → ${result.targetLanguage.toUpperCase()}`; + + return [ + `## ${direction}`, + "", + asMarkdownCodeBlock(result.text), + "", + "---", + "", + "**Source**", + "", + asMarkdownCodeBlock(sourceText), + ].join("\n"); +} diff --git a/extensions/ray-tools/src/tools/translate/provider.test.ts b/extensions/ray-tools/src/tools/translate/provider.test.ts new file mode 100644 index 00000000000..12d3e64767b --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/provider.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + GoogleTranslateProvider, + parseGoogleTranslateResponse, +} from "./provider"; + +const russianResponse = { + sentences: [{ trans: "Hello, how are you?", orig: "Привет, как дела?" }], + src: "ru", +}; + +describe("Google translation provider", () => { + it("parses the JSON response returned by the endpoint", () => { + expect(parseGoogleTranslateResponse(russianResponse)).toEqual({ + text: "Hello, how are you?", + detectedLanguage: "ru", + }); + }); + + it("requests the opposite language for Russian input", async () => { + const requests: Array<{ url: string; init?: RequestInit }> = []; + const fetcher: typeof fetch = async (input, init) => { + requests.push({ url: String(input), init }); + return new Response(JSON.stringify(russianResponse), { status: 200 }); + }; + + const provider = new GoogleTranslateProvider(fetcher); + const result = await provider.translate("Привет, как дела?"); + + expect(result).toMatchObject({ + text: "Hello, how are you?", + sourceLanguage: "ru", + targetLanguage: "en", + }); + expect(requests[0]?.url).toContain("sl=ru"); + expect(requests[0]?.url).toContain("tl=en"); + }); + + it("exposes a readable error when the endpoint fails", async () => { + const fetcher: typeof fetch = async () => + new Response("upstream failed", { + status: 503, + statusText: "Service Unavailable", + }); + const provider = new GoogleTranslateProvider(fetcher); + + await expect(provider.translate("Hello")).rejects.toThrow( + "Translation service returned HTTP 503", + ); + }); + + it("times out while reading a slow response body", async () => { + vi.useFakeTimers(); + + try { + const fetcher: typeof fetch = async (_input, init) => { + const signal = init?.signal; + + return { + ok: true, + status: 200, + json: () => + new Promise((_, reject) => { + signal?.addEventListener( + "abort", + () => reject(new Error("aborted")), + { once: true }, + ); + }), + } as Response; + }; + + const promise = new GoogleTranslateProvider(fetcher).translate("Hello"); + const rejection = expect(promise).rejects.toThrow( + "Translation service request timed out", + ); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(15_000); + + await rejection; + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/extensions/ray-tools/src/tools/translate/provider.ts b/extensions/ray-tools/src/tools/translate/provider.ts new file mode 100644 index 00000000000..3589516d368 --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/provider.ts @@ -0,0 +1,134 @@ +import { + detectLanguage, + getTargetLanguage, + type SupportedLanguage, +} from "./domain"; +import type { TranslationProvider, TranslationResult } from "./types"; + +const GOOGLE_TRANSLATE_ENDPOINT = + "https://translate.googleapis.com/translate_a/single"; +const TRANSLATION_TIMEOUT_MS = 15_000; + +type GoogleSentence = { + trans?: unknown; +}; + +type ParsedGoogleResponse = { + text: string; + detectedLanguage?: SupportedLanguage; +}; + +function asSupportedLanguage(value: unknown): SupportedLanguage | undefined { + if (typeof value !== "string") { + return undefined; + } + + const language = value.toLowerCase().split("-")[0]; + return language === "ru" || language === "en" ? language : undefined; +} + +export function parseGoogleTranslateResponse( + payload: unknown, +): ParsedGoogleResponse { + if ( + typeof payload === "object" && + payload !== null && + !Array.isArray(payload) + ) { + const response = payload as { sentences?: unknown; src?: unknown }; + if (Array.isArray(response.sentences)) { + const text = response.sentences + .map((sentence) => (sentence as GoogleSentence)?.trans) + .filter((part): part is string => typeof part === "string") + .join(""); + + if (text.length > 0) { + return { text, detectedLanguage: asSupportedLanguage(response.src) }; + } + } + } + + if (Array.isArray(payload) && Array.isArray(payload[0])) { + const text = payload[0] + .map((sentence) => (Array.isArray(sentence) ? sentence[0] : undefined)) + .filter((part): part is string => typeof part === "string") + .join(""); + + if (text.length > 0) { + return { text, detectedLanguage: asSupportedLanguage(payload[2]) }; + } + } + + throw new Error("Translation service returned an empty response"); +} + +export class GoogleTranslateProvider implements TranslationProvider { + constructor( + private readonly fetcher: typeof fetch = fetch, + private readonly endpoint = GOOGLE_TRANSLATE_ENDPOINT, + ) {} + + async translate(text: string): Promise { + const localSourceLanguage = detectLanguage(text); + const targetLanguage = getTargetLanguage(localSourceLanguage); + const url = new URL(this.endpoint); + + url.searchParams.set("client", "gtx"); + // Keep the requested direction consistent with the local detector. Using + // Google's automatic source detection here can disagree for mixed-script + // text and produce results such as EN → EN. + url.searchParams.set("sl", localSourceLanguage); + url.searchParams.set("tl", targetLanguage); + url.searchParams.set("dt", "t"); + url.searchParams.set("dj", "1"); + url.searchParams.set("q", text); + + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + TRANSLATION_TIMEOUT_MS, + ); + + try { + let response: Response; + try { + response = await this.fetcher(url.toString(), { + headers: { Accept: "application/json" }, + signal: controller.signal, + }); + } catch { + if (controller.signal.aborted) { + throw new Error("Translation service request timed out"); + } + + throw new Error("Unable to reach translation service"); + } + + if (!response.ok) { + throw new Error(`Translation service returned HTTP ${response.status}`); + } + + let payload: unknown; + try { + payload = await response.json(); + } catch { + if (controller.signal.aborted) { + throw new Error("Translation service request timed out"); + } + + throw new Error("Translation service returned invalid JSON"); + } + + const parsed = parseGoogleTranslateResponse(payload); + + return { + text: parsed.text, + sourceLanguage: parsed.detectedLanguage ?? localSourceLanguage, + targetLanguage, + provider: "google-web", + }; + } finally { + clearTimeout(timeout); + } + } +} diff --git a/extensions/ray-tools/src/tools/translate/service.test.ts b/extensions/ray-tools/src/tools/translate/service.test.ts new file mode 100644 index 00000000000..9734f55415d --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/service.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { TranslationService } from "./service"; +import type { TranslationProvider } from "./types"; + +describe("TranslationService", () => { + it("rejects blank input before calling a provider", async () => { + let calls = 0; + const provider: TranslationProvider = { + translate: async () => { + calls += 1; + throw new Error("must not be called"); + }, + }; + + await expect( + new TranslationService(provider).translate(" \n "), + ).rejects.toThrow("Enter text to translate"); + expect(calls).toBe(0); + }); +}); diff --git a/extensions/ray-tools/src/tools/translate/service.ts b/extensions/ray-tools/src/tools/translate/service.ts new file mode 100644 index 00000000000..22860ceae63 --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/service.ts @@ -0,0 +1,14 @@ +import type { TranslationProvider, TranslationResult } from "./types"; + +export class TranslationService { + constructor(private readonly provider: TranslationProvider) {} + + async translate(text: string): Promise { + const normalizedText = text.trim(); + if (!normalizedText) { + throw new Error("Enter text to translate"); + } + + return this.provider.translate(normalizedText); + } +} diff --git a/extensions/ray-tools/src/tools/translate/types.ts b/extensions/ray-tools/src/tools/translate/types.ts new file mode 100644 index 00000000000..156663461de --- /dev/null +++ b/extensions/ray-tools/src/tools/translate/types.ts @@ -0,0 +1,12 @@ +import type { SupportedLanguage } from "./domain"; + +export interface TranslationResult { + text: string; + sourceLanguage: SupportedLanguage; + targetLanguage: SupportedLanguage; + provider: string; +} + +export interface TranslationProvider { + translate(text: string): Promise; +} diff --git a/extensions/ray-tools/src/translate.tsx b/extensions/ray-tools/src/translate.tsx new file mode 100644 index 00000000000..e0a2e813913 --- /dev/null +++ b/extensions/ray-tools/src/translate.tsx @@ -0,0 +1 @@ +export { default } from "./commands/translate"; diff --git a/extensions/ray-tools/tsconfig.json b/extensions/ray-tools/tsconfig.json new file mode 100644 index 00000000000..f82d25f69ad --- /dev/null +++ b/extensions/ray-tools/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*", "raycast-env.d.ts", "vitest.config.mts"], + "compilerOptions": { + "lib": ["es2022", "dom"], + "module": "node16", + "moduleResolution": "node16", + "target": "es2022", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "types": ["node", "vitest/globals"] + } +} diff --git a/extensions/ray-tools/vitest.config.mts b/extensions/ray-tools/vitest.config.mts new file mode 100644 index 00000000000..bc2248f83b8 --- /dev/null +++ b/extensions/ray-tools/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + }, +});