diff --git a/extensions/apple-tv-remote/.gitignore b/extensions/apple-tv-remote/.gitignore
new file mode 100644
index 00000000000..b4bea212e48
--- /dev/null
+++ b/extensions/apple-tv-remote/.gitignore
@@ -0,0 +1,15 @@
+# Dependencies
+node_modules/
+
+# Raycast
+raycast-env.d.ts
+.raycast-swift-build
+.swiftpm
+compiled_raycast_swift
+compiled_raycast_rust
+
+# Build
+dist/
+
+# Misc
+.DS_Store
diff --git a/extensions/apple-tv-remote/.prettierrc b/extensions/apple-tv-remote/.prettierrc
new file mode 100644
index 00000000000..fc0f5030683
--- /dev/null
+++ b/extensions/apple-tv-remote/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "printWidth": 120,
+ "singleQuote": false
+}
diff --git a/extensions/apple-tv-remote/CHANGELOG.md b/extensions/apple-tv-remote/CHANGELOG.md
new file mode 100644
index 00000000000..39d1c687176
--- /dev/null
+++ b/extensions/apple-tv-remote/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Apple TV Remote Changelog
+
+## [Apple TV Remote] - 2026-07-01
+
+- Visual Apple TV remote with a persistent connection — clickable button grid plus a no-modifier keyboard layer (WASD/HJKL, F select, Space play/pause) and ⌥-shortcuts
+- Bonjour discovery + on-screen PIN pairing, with manual IP/port fallback
+- Full Siri Remote function set ported from pyatv: context menu (hold-select), app switcher, Control Center, ±10s skip, sleep/wake, screensaver
+- App launcher grid backed by the device's live app list, with hotkey-able Quicklinks per app
+- Ask Apple TV: one-shot natural-language commands (pause, open an app, type text, play a show)
+- Title playback via real JustWatch-resolved deep links where tvOS supports them, with an Apple TV universal-search typing flow for Netflix and unresolved titles
+- Menu bar quick controls and per-key commands (disabled by default) for global hotkeys
+- AI tools: press remote keys, launch apps, play content, sleep/wake — usable from Raycast AI chat and Quick AI
diff --git a/extensions/apple-tv-remote/LICENSE b/extensions/apple-tv-remote/LICENSE
new file mode 100644
index 00000000000..c07a323f726
--- /dev/null
+++ b/extensions/apple-tv-remote/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Cameron Schroeder
+
+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/apple-tv-remote/NOTICE b/extensions/apple-tv-remote/NOTICE
new file mode 100644
index 00000000000..ab977c823cc
--- /dev/null
+++ b/extensions/apple-tv-remote/NOTICE
@@ -0,0 +1,17 @@
+Apple TV Remote for Raycast
+Copyright (c) 2026 Cameron Schroeder — MIT License (see LICENSE)
+
+This project builds on the following third-party work:
+
+1. pyatv — https://github.com/postlund/pyatv
+ Copyright (c) 2020 Pierre Ståhl — MIT License
+ The Companion-protocol payloads implemented in src/lib/companion-extras.ts
+ (app launching, app listing, power, Control Center, hold gestures, skip)
+ are ported from pyatv's reference implementation and protocol
+ documentation (https://pyatv.dev/documentation/protocols/).
+
+2. @bharper/atv-js — https://github.com/bsharper/atvjs
+ Copyright (c) Brian Harper; carries the MIT license of pyatv
+ (Copyright (c) 2020 Pierre Ståhl), of which it is a derivative
+ TypeScript port. Used as the Companion-protocol transport
+ (discovery, pairing, encrypted session, remote keys, text input).
diff --git a/extensions/apple-tv-remote/README.md b/extensions/apple-tv-remote/README.md
new file mode 100644
index 00000000000..543fb191aa7
--- /dev/null
+++ b/extensions/apple-tv-remote/README.md
@@ -0,0 +1,95 @@
+# Apple TV Remote
+
+Control your Apple TV from Raycast. No Python, no helper apps, nothing to install beyond the extension. It talks to the Apple TV directly over Apple's Companion protocol in pure TypeScript.
+
+
+
+## Features
+
+- **Apple TV Remote.** A clickable on-screen remote that keeps one live connection open, so every press lands instantly. D-pad, select, back, home, hold-select context menu, app switcher, Control Center, playback, 10-second skip, and typing into TV search fields.
+- **Launch Apple TV App.** A grid of the apps on your Apple TV. Open any of them, or save one as a hotkey-able Quicklink.
+- **Ask Apple TV.** One-off natural language, like `pause`, `open netflix`, `play severance`, `type stranger things`, or `sleep`.
+- **Menu bar.** Quick taps for play/pause, skip, navigation, opening apps, and sleep/wake, plus one click through to the full remote.
+- **Per-key hotkey commands.** Every remote function is also its own command (most are off by default). Enable the ones you want and give them global hotkeys.
+- **AI tools.** Drive it from Raycast AI, like `@apple-tv-remote pause` or `@apple-tv-remote play Rick and Morty on Netflix` (needs Raycast Pro).
+
+| Every function, one search away | Remote in your menu bar |
+| ------------------------------- | ------------------------------ |
+|  |  |
+
+## Setup
+
+1. Run **Set up Apple TV**.
+2. Pick your Apple TV from the list, or add it by IP and port if discovery is blocked on your network.
+3. Type the 4-digit PIN that appears on the TV.
+
+Pairing is interactive because the Apple TV shows the PIN on screen, which is why setup is a command rather than an extension preference.
+
+## The remote
+
+Open **Apple TV Remote** and either click the buttons or use the keyboard.
+
+| Control | Bare key | With ⌥ |
+| -------------------------- | ------------------------------------ | ------------------- |
+| Navigate | `W` `A` `S` `D` (or `H` `J` `K` `L`) | `⌥↑` `⌥↓` `⌥←` `⌥→` |
+| Select | `F` (or `G`) | `⌥↩` |
+| Back | `B` | `⌥⌫` |
+| Home | `Q` | |
+| Play/Pause | `Space` | `⌥P` |
+| Skip 10s | `,` / `.` | |
+| Previous / Next | `[` / `]` | |
+| Context menu (hold select) | `V` | |
+| App switcher | `X` | |
+| Control Center | `C` | |
+| Type text on TV | `T` | |
+| Screensaver | | `⌥S` |
+
+The bare keys work because the view treats your typing as button presses, so no modifiers are needed. Volume has optional hotkey commands (`Volume Up` and `Volume Down`), but they only do anything when the Apple TV controls volume over HDMI-CEC. Most TVs handle volume themselves.
+
+## Playing a show
+
+`play
[on ]` looks the title up on JustWatch and opens a real provider deep link where tvOS supports one (Apple TV+, Disney+, Max, YouTube, and others). It does not guess at app IDs. Netflix dropped tvOS deep links in late 2025, so for Netflix and for anything that does not resolve, the extension opens the Apple TV's universal Search and types the title for you, then you pick the result on screen. The **Search Automation** preference sets how far it goes.
+
+## Preferences
+
+- **Streaming Country.** Two-letter code for the where-to-watch lookup. Leave it blank to detect your Mac's region.
+- **Search Automation.** What happens after a title is typed into Apple TV Search: stop there, open the top result (the default), or also press Play. Pressing Play can start the wrong app, since the title page usually lists several.
+- **Connection Timeout.** How long a command waits to reach the Apple TV.
+
+## How it works
+
+The extension speaks Apple's Companion protocol, the same one the iOS Remote uses, straight from Raycast's Node runtime through [`@bharper/atv-js`](https://github.com/bsharper/atvjs), a pure-TypeScript port of [pyatv](https://pyatv.dev). Discovery is Bonjour, pairing is HAP SRP with the on-screen PIN, and the session is chacha20-poly1305 encrypted. App launching, app listing, sleep/wake, Control Center, hold-select, and 10-second skip are built on top of the library in this extension, ported from pyatv's reference implementation.
+
+### Credential storage
+
+Pairing produces machine-generated key material rather than a password you type, so it lives in Raycast's encrypted [LocalStorage](https://developers.raycast.com/api-reference/storage), scoped to this extension. The extension never touches the macOS Keychain, and it sends nothing off your local network. The only internet request is the optional JustWatch title lookup.
+
+## Development
+
+```bash
+npm install
+npm run dev # ray develop, hot-reloads into Raycast
+npm run lint # ray lint
+npm run build # ray build
+```
+
+| Module | Role |
+| -------------------------------------- | ---------------------------------------------------------------------------------------------- |
+| `src/lib/connection.ts` | Persistent session for the remote view, plus per-command `withConnection()` |
+| `src/lib/companion-extras.ts` | Companion payloads ported from pyatv (launch app, app list, power, Control Center, hold, skip) |
+| `src/lib/credentials.ts`, `devices.ts` | Pairing credentials and selected device in LocalStorage |
+| `src/lib/justwatch.ts` | Title to deep-link resolution (keyless GraphQL, cached) |
+| `src/lib/play-flow.ts` | Deep link, then universal-search typing, then app launch |
+| `src/lib/deep-links.ts` | Curated app and bundle map, plus installed-app cache |
+| `src/lib/errors.ts` | Typed errors turned into actionable toasts |
+
+## Credits
+
+- [pyatv](https://pyatv.dev) by Pierre Ståhl (MIT). The reference implementation and protocol docs this extension is built on. The Companion payloads for app launching, app listing, power, and gestures are ported from it.
+- [`@bharper/atv-js`](https://github.com/bsharper/atvjs) by Brian Harper (MIT). The TypeScript Companion-protocol port, itself derived from pyatv (© Pierre Ståhl).
+
+See [NOTICE](NOTICE) for third-party license details.
+
+## License
+
+MIT
diff --git a/extensions/apple-tv-remote/assets/blank.png b/extensions/apple-tv-remote/assets/blank.png
new file mode 100644
index 00000000000..65a755c5f1c
Binary files /dev/null and b/extensions/apple-tv-remote/assets/blank.png differ
diff --git a/extensions/apple-tv-remote/assets/command-icon.png b/extensions/apple-tv-remote/assets/command-icon.png
new file mode 100644
index 00000000000..0b03d7cdda5
Binary files /dev/null and b/extensions/apple-tv-remote/assets/command-icon.png differ
diff --git a/extensions/apple-tv-remote/assets/menubar-icon.png b/extensions/apple-tv-remote/assets/menubar-icon.png
new file mode 100644
index 00000000000..3ba292cfb90
Binary files /dev/null and b/extensions/apple-tv-remote/assets/menubar-icon.png differ
diff --git a/extensions/apple-tv-remote/assets/menubar-icon@dark.png b/extensions/apple-tv-remote/assets/menubar-icon@dark.png
new file mode 100644
index 00000000000..cd31196c328
Binary files /dev/null and b/extensions/apple-tv-remote/assets/menubar-icon@dark.png differ
diff --git a/extensions/apple-tv-remote/eslint.config.js b/extensions/apple-tv-remote/eslint.config.js
new file mode 100644
index 00000000000..543274e97ec
--- /dev/null
+++ b/extensions/apple-tv-remote/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/apple-tv-remote/media/commands.png b/extensions/apple-tv-remote/media/commands.png
new file mode 100644
index 00000000000..87a184898da
Binary files /dev/null and b/extensions/apple-tv-remote/media/commands.png differ
diff --git a/extensions/apple-tv-remote/media/menubar.png b/extensions/apple-tv-remote/media/menubar.png
new file mode 100644
index 00000000000..a9864e469d9
Binary files /dev/null and b/extensions/apple-tv-remote/media/menubar.png differ
diff --git a/extensions/apple-tv-remote/media/remote.png b/extensions/apple-tv-remote/media/remote.png
new file mode 100644
index 00000000000..c5f306c25a4
Binary files /dev/null and b/extensions/apple-tv-remote/media/remote.png differ
diff --git a/extensions/apple-tv-remote/metadata/apple-tv-remote-1.png b/extensions/apple-tv-remote/metadata/apple-tv-remote-1.png
new file mode 100644
index 00000000000..c5f306c25a4
Binary files /dev/null and b/extensions/apple-tv-remote/metadata/apple-tv-remote-1.png differ
diff --git a/extensions/apple-tv-remote/metadata/apple-tv-remote-2.png b/extensions/apple-tv-remote/metadata/apple-tv-remote-2.png
new file mode 100644
index 00000000000..87a184898da
Binary files /dev/null and b/extensions/apple-tv-remote/metadata/apple-tv-remote-2.png differ
diff --git a/extensions/apple-tv-remote/metadata/apple-tv-remote-3.png b/extensions/apple-tv-remote/metadata/apple-tv-remote-3.png
new file mode 100644
index 00000000000..a9864e469d9
Binary files /dev/null and b/extensions/apple-tv-remote/metadata/apple-tv-remote-3.png differ
diff --git a/extensions/apple-tv-remote/package-lock.json b/extensions/apple-tv-remote/package-lock.json
new file mode 100644
index 00000000000..0851b12319b
--- /dev/null
+++ b/extensions/apple-tv-remote/package-lock.json
@@ -0,0 +1,3140 @@
+{
+ "name": "apple-tv-remote",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "apple-tv-remote",
+ "license": "MIT",
+ "dependencies": {
+ "@bharper/atv-js": "0.3.4",
+ "@raycast/api": "^1.104.19",
+ "@raycast/utils": "^2.2.6"
+ },
+ "devDependencies": {
+ "@raycast/eslint-config": "^2.0.4",
+ "@types/node": "^22.13.10",
+ "@types/react": "^19.0.10",
+ "eslint": "^9.22.0",
+ "prettier": "^3.5.3",
+ "typescript": "^5.8.2"
+ }
+ },
+ "node_modules/@bharper/atv-js": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@bharper/atv-js/-/atv-js-0.3.4.tgz",
+ "integrity": "sha512-nwlC26zbqVT35zajtbEBZgqirAHL5HX00zEUT33vOKUzdIQXriQL/t1LIt3c8zecSIxjRT8tIw9kaVGgK5ATYQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@noble/ciphers": "^2.1.1",
+ "bplist-creator": "^0.1.0",
+ "bplist-parser": "^0.3.2",
+ "fast-srp-hap": "^2.0.4",
+ "multicast-dns": "^7.2.5"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "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.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@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/@leichtgewicht/ip-codec": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+ "license": "MIT"
+ },
+ "node_modules/@noble/ciphers": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz",
+ "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@oclif/core": {
+ "version": "4.11.4",
+ "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.4.tgz",
+ "integrity": "sha512-URwiQ5ALx/sJ2iH4vzXEd+H4K6NAI7LRs6Jag3hrgKEpGmaE6alfRC8qjO4GIgb6A3ACaJumqP9twi/M9ywdHQ==",
+ "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.16",
+ "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.50",
+ "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.50.tgz",
+ "integrity": "sha512-SQRIJSYue/1tIn7X55W/97gTb8UkSoHeFAcBng2r2YMJyWj8uB1DtFl28D8BDXPQXPTiPK89hQGejoT7RdkR2w==",
+ "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.50",
+ "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.50.tgz",
+ "integrity": "sha512-rNCG4hUm+kPXFbhJfAVk/fZ3OdWJYwBDASlyX8CqOLP0MssjIGl7iEgfZz7TMuZFa+KucupKU5NRSc0KWfPTQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oclif/core": "^4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@oclif/plugin-not-found": {
+ "version": "3.2.87",
+ "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.87.tgz",
+ "integrity": "sha512-lKyZ4INrx5vB14HNWIkM6Vla/4rWVhOA2U7uCAj6gEBg36/KVmwYXxpZ9ckzZS0+jtLE84TVqS8NCYEhQiQojw==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/prompts": "^7.10.1",
+ "@oclif/core": "^4.11.4",
+ "ansis": "^3.17.0",
+ "fast-levenshtein": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@raycast/api": {
+ "version": "1.104.19",
+ "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.104.19.tgz",
+ "integrity": "sha512-SAVg56BAzxZGy/OPQ0jekUG3pJaoX5pCqleALvFo9JRE7P2tvKoglWnYRcIJArRhLlvV8FtFNMDafd+NNwXXCw==",
+ "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.1.1",
+ "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.1.1.tgz",
+ "integrity": "sha512-W0kxF+FJ+BYQn0EKIV739j2ZrHEtjo/LclsoZgUWg3t364Dq75XKcjqYFYx+59/DBaamY0amdajlfuDAf6veAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint/js": "^9.36.0",
+ "@raycast/eslint-plugin": "^2.1.1",
+ "eslint-config-prettier": "^10.1.8",
+ "globals": "^16.4.0",
+ "typescript-eslint": "^8.45.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.23.0",
+ "prettier": ">=2",
+ "typescript": ">=4"
+ }
+ },
+ "node_modules/@raycast/eslint-plugin": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.1.1.tgz",
+ "integrity": "sha512-r2gs8uIlNp6I2mLOyN/kReGlvigzEeuyQPl4yw7nwLy8Zxjfjhg8txMViaBux8juBWBxbSWq/IfW6ZA50oeOHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^8.26.1"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.23.0"
+ }
+ },
+ "node_modules/@raycast/utils": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-2.2.6.tgz",
+ "integrity": "sha512-/6NUftsaNjboTOhTiadRhQ+hgfsg9AxKdYCpGGbwfUghTX9M+ljbGBesCxQ+X5GBIBM+9lH3ma7GMwZPSPzJgw==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ },
+ "peerDependencies": {
+ "@raycast/api": ">=1.99.4",
+ "react": ">=19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "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": "22.19.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz",
+ "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
+ "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/type-utils": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "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.60.1",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
+ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
+ "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.60.1",
+ "@typescript-eslint/types": "^8.60.1",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
+ "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1"
+ },
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
+ "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
+ "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
+ "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
+ "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.60.1",
+ "@typescript-eslint/tsconfig-utils": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
+ "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1"
+ },
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
+ "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "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/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "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/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/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/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/bplist-creator": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.1.tgz",
+ "integrity": "sha512-Ese7052fdWrxp/vqSJkydgx/1MdBnNOCV2XVfbmdGWD2H6EYza+Q4pyYSuVSnCUD22hfI/BFI4jHaC3NLXLlJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "stream-buffers": "2.2.x"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
+ "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "1.6.x"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
+ "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/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/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/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+ "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.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": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-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": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-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/fast-srp-hap": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz",
+ "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "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/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/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/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/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/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "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/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/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/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "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.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "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/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/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.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
+ "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+ "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/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/stream-buffers": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
+ "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "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/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "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/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "license": "MIT"
+ },
+ "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/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": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
+ "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.60.1",
+ "@typescript-eslint/parser": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1"
+ },
+ "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/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/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/apple-tv-remote/package.json b/extensions/apple-tv-remote/package.json
new file mode 100644
index 00000000000..b04012c0d2e
--- /dev/null
+++ b/extensions/apple-tv-remote/package.json
@@ -0,0 +1,429 @@
+{
+ "$schema": "https://www.raycast.com/schemas/extension.json",
+ "name": "apple-tv-remote",
+ "title": "Apple TV Remote",
+ "description": "Control your Apple TV from Raycast with a visual remote, app launcher, deep links, and AI commands — no external installs required.",
+ "icon": "command-icon.png",
+ "author": "camschroedes",
+ "categories": [
+ "Media",
+ "Applications"
+ ],
+ "platforms": [
+ "macOS"
+ ],
+ "license": "MIT",
+ "commands": [
+ {
+ "name": "remote",
+ "title": "Apple TV Remote",
+ "subtitle": "Apple TV",
+ "description": "On-screen remote with d-pad, playback, volume, and text input.",
+ "mode": "view"
+ },
+ {
+ "name": "ask",
+ "title": "Ask Apple TV",
+ "subtitle": "Apple TV",
+ "description": "One-off natural-language commands: pause, open Netflix, play a show.",
+ "mode": "no-view",
+ "arguments": [
+ {
+ "name": "query",
+ "placeholder": "pause · open Netflix · play Severance",
+ "type": "text",
+ "required": true
+ }
+ ]
+ },
+ {
+ "name": "setup",
+ "title": "Set up Apple TV",
+ "subtitle": "Apple TV",
+ "description": "Discover and pair with an Apple TV on your network.",
+ "mode": "view"
+ },
+ {
+ "name": "launch-app",
+ "title": "Launch Apple TV App",
+ "subtitle": "Apple TV",
+ "description": "Browse and open apps installed on your Apple TV.",
+ "mode": "view"
+ },
+ {
+ "name": "play-pause",
+ "title": "Play/Pause",
+ "subtitle": "Apple TV",
+ "description": "Toggle playback on your Apple TV.",
+ "mode": "no-view"
+ },
+ {
+ "name": "home",
+ "title": "Go to Home Screen",
+ "subtitle": "Apple TV",
+ "description": "Jump to the Apple TV home screen.",
+ "mode": "no-view"
+ },
+ {
+ "name": "menubar",
+ "title": "Apple TV Menu Bar",
+ "subtitle": "Apple TV",
+ "description": "Quick Apple TV controls from your menu bar.",
+ "mode": "menu-bar"
+ },
+ {
+ "name": "up",
+ "title": "Up",
+ "subtitle": "Apple TV",
+ "description": "Press Up on the Apple TV remote.",
+ "mode": "no-view",
+ "keywords": [
+ "atv"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "down",
+ "title": "Down",
+ "subtitle": "Apple TV",
+ "description": "Press Down on the Apple TV remote.",
+ "mode": "no-view",
+ "keywords": [
+ "atv"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "left",
+ "title": "Left",
+ "subtitle": "Apple TV",
+ "description": "Press Left on the Apple TV remote.",
+ "mode": "no-view",
+ "keywords": [
+ "atv"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "right",
+ "title": "Right",
+ "subtitle": "Apple TV",
+ "description": "Press Right on the Apple TV remote.",
+ "mode": "no-view",
+ "keywords": [
+ "atv"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "select",
+ "title": "Select",
+ "subtitle": "Apple TV",
+ "description": "Press Select on the Apple TV remote.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "ok"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "back",
+ "title": "Back",
+ "subtitle": "Apple TV",
+ "description": "Go back (Menu) on the Apple TV.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "menu"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "volume-up",
+ "title": "Volume up",
+ "subtitle": "Apple TV",
+ "description": "Turn the Apple TV volume up.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "louder"
+ ]
+ },
+ {
+ "name": "volume-down",
+ "title": "Volume Down",
+ "subtitle": "Apple TV",
+ "description": "Turn the Apple TV volume down.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "quieter"
+ ]
+ },
+ {
+ "name": "skip-forward",
+ "title": "Skip Forward 10 Seconds",
+ "subtitle": "Apple TV",
+ "description": "Skip forward in the current video.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "seek"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "skip-back",
+ "title": "Skip Back 10 Seconds",
+ "subtitle": "Apple TV",
+ "description": "Skip backward in the current video.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "seek",
+ "rewind"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "context-menu",
+ "title": "Open Context Menu",
+ "subtitle": "Apple TV",
+ "description": "Long-press select on the Apple TV to open the context menu for the focused item.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "hold"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "app-switcher",
+ "title": "App Switcher",
+ "subtitle": "Apple TV",
+ "description": "Open the Apple TV app switcher.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "multitask"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "control-center",
+ "title": "Control Center",
+ "subtitle": "Apple TV",
+ "description": "Open Control Center on the Apple TV.",
+ "mode": "no-view",
+ "keywords": [
+ "atv"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "sleep",
+ "title": "Sleep Apple TV",
+ "subtitle": "Apple TV",
+ "description": "Put the Apple TV to sleep.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "off"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "wake",
+ "title": "Wake Apple TV",
+ "subtitle": "Apple TV",
+ "description": "Wake the Apple TV up.",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "on"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "screensaver",
+ "title": "Start Screensaver",
+ "subtitle": "Apple TV",
+ "description": "Start the Apple TV screensaver (can be flaky on some models).",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "aerial"
+ ],
+ "disabledByDefault": true
+ },
+ {
+ "name": "type-text",
+ "title": "Type Text on Apple TV",
+ "subtitle": "Apple TV",
+ "description": "Send text into the focused field on the Apple TV (e.g. a search box).",
+ "mode": "no-view",
+ "keywords": [
+ "atv",
+ "keyboard",
+ "input"
+ ],
+ "arguments": [
+ {
+ "name": "text",
+ "placeholder": "text to type",
+ "type": "text",
+ "required": true
+ }
+ ]
+ }
+ ],
+ "tools": [
+ {
+ "name": "remote-key",
+ "title": "Press Remote Key",
+ "description": "Press a button on the Apple TV remote: up, down, left, right, select, menu (back), home, play_pause, volume_up, volume_down, next, previous."
+ },
+ {
+ "name": "launch-app",
+ "title": "Launch App",
+ "description": "Open an app on the Apple TV by name, e.g. Netflix, YouTube, Disney+."
+ },
+ {
+ "name": "play-content",
+ "title": "Play Content",
+ "description": "Open a specific show or movie on the Apple TV, optionally in a specific streaming app. Resolves the title to a deep link when possible, otherwise opens the app."
+ },
+ {
+ "name": "power",
+ "title": "Power",
+ "description": "Put the Apple TV to sleep or wake it up."
+ }
+ ],
+ "ai": {
+ "instructions": "You control the user's Apple TV. For playback/navigation requests (pause, play, up, back, home, volume) call remote-key with the matching key ('pause' and 'play' both map to play_pause). When the user names an app, call launch-app. When the user names a show or movie, call play-content (include the app if they named one). For sleep/wake/'turn off'/'turn on' call power. Never invent bundle IDs or deep-link URLs; the tools resolve real identifiers themselves. If a request is ambiguous (which show, which app), ask a short clarifying question first. Relay play-content result messages verbatim; they state exactly what happened.",
+ "evals": [
+ {
+ "input": "@apple-tv-remote pause",
+ "mocks": {
+ "remote-key": "Pressed play_pause"
+ },
+ "expected": [
+ {
+ "callsTool": {
+ "name": "remote-key",
+ "arguments": {
+ "key": "play_pause"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "input": "@apple-tv-remote open Netflix",
+ "mocks": {
+ "launch-app": "Launched Netflix"
+ },
+ "expected": [
+ {
+ "callsTool": {
+ "name": "launch-app",
+ "arguments": {
+ "app": "Netflix"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "input": "@apple-tv-remote play Rick and Morty on Netflix",
+ "mocks": {
+ "play-content": "Opened Rick and Morty on Netflix"
+ },
+ "expected": [
+ {
+ "callsTool": {
+ "name": "play-content"
+ }
+ }
+ ]
+ },
+ {
+ "input": "@apple-tv-remote put the tv to sleep",
+ "mocks": {
+ "power": "Apple TV is going to sleep"
+ },
+ "expected": [
+ {
+ "callsTool": {
+ "name": "power",
+ "arguments": {
+ "action": "sleep"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "preferences": [
+ {
+ "name": "country",
+ "title": "Streaming Country",
+ "description": "Two-letter country code used to find where shows are streaming (e.g. US, AU, GB). Leave blank to auto-detect from your Mac's region.",
+ "type": "textfield",
+ "required": false
+ },
+ {
+ "name": "searchAutomation",
+ "title": "Search Automation",
+ "description": "How far to go after typing a title into Apple TV Search. 'Open top result' stops on the title page so you pick the streaming app. Pressing Play blindly can start the wrong provider.",
+ "type": "dropdown",
+ "default": "open",
+ "required": false,
+ "data": [
+ {
+ "title": "Type the title only",
+ "value": "type"
+ },
+ {
+ "title": "Open top result (recommended)",
+ "value": "open"
+ },
+ {
+ "title": "Open and press Play (may pick the wrong app)",
+ "value": "play"
+ }
+ ]
+ },
+ {
+ "name": "connectTimeout",
+ "title": "Connection Timeout",
+ "description": "How long commands wait to reach the Apple TV (milliseconds).",
+ "type": "textfield",
+ "default": "8000",
+ "required": false
+ }
+ ],
+ "dependencies": {
+ "@bharper/atv-js": "0.3.4",
+ "@raycast/api": "^1.104.19",
+ "@raycast/utils": "^2.2.6"
+ },
+ "devDependencies": {
+ "@raycast/eslint-config": "^2.0.4",
+ "@types/node": "^22.13.10",
+ "@types/react": "^19.0.10",
+ "eslint": "^9.22.0",
+ "prettier": "^3.5.3",
+ "typescript": "^5.8.2"
+ },
+ "scripts": {
+ "build": "ray build",
+ "dev": "ray develop",
+ "fix-lint": "ray lint --fix",
+ "lint": "ray lint",
+ "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
+ "publish": "npx @raycast/api@latest publish"
+ }
+}
diff --git a/extensions/apple-tv-remote/src/app-switcher.ts b/extensions/apple-tv-remote/src/app-switcher.ts
new file mode 100644
index 00000000000..55182ed9f19
--- /dev/null
+++ b/extensions/apple-tv-remote/src/app-switcher.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { appSwitcher } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => appSwitcher(conn));
+ await showHUD("🗂 App Switcher");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/ask.ts b/extensions/apple-tv-remote/src/ask.ts
new file mode 100644
index 00000000000..b2a338dc48c
--- /dev/null
+++ b/extensions/apple-tv-remote/src/ask.ts
@@ -0,0 +1,161 @@
+import { LaunchProps, showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+import {
+ appSwitcher,
+ controlCenter,
+ launchApp,
+ longPressSelect,
+ skipBy,
+ sleepDevice,
+ startScreensaver,
+ wakeDevice,
+} from "./lib/companion-extras";
+import { setText } from "@bharper/atv-js";
+import { loadCachedApps, resolveAppName } from "./lib/deep-links";
+import { playContent } from "./lib/play-flow";
+
+/**
+ * One-shot natural-language command: "pause", "open Netflix",
+ * "play Rick and Morty on Netflix". Common phrases are handled instantly with
+ * no AI; Raycast AI (when available) only resolves show titles to deep links.
+ */
+
+const KEY_PHRASES: Record = {
+ pause: RemoteKey.PlayPause,
+ play: RemoteKey.PlayPause,
+ resume: RemoteKey.PlayPause,
+ up: RemoteKey.Up,
+ down: RemoteKey.Down,
+ left: RemoteKey.Left,
+ right: RemoteKey.Right,
+ select: RemoteKey.Select,
+ ok: RemoteKey.Select,
+ back: RemoteKey.Menu,
+ menu: RemoteKey.Menu,
+ home: RemoteKey.Home,
+ next: RemoteKey.Next,
+ skip: RemoteKey.Next,
+ previous: RemoteKey.Previous,
+ "volume up": RemoteKey.VolumeUp,
+ louder: RemoteKey.VolumeUp,
+ "volume down": RemoteKey.VolumeDown,
+ quieter: RemoteKey.VolumeDown,
+};
+
+const HUD_LABELS: Partial> = {
+ [RemoteKey.PlayPause]: "⏯ Play/Pause",
+ [RemoteKey.Home]: "🏠 Home",
+ [RemoteKey.VolumeUp]: "🔊 Volume Up",
+ [RemoteKey.VolumeDown]: "🔉 Volume Down",
+};
+
+async function pressKey(key: RemoteKey): Promise {
+ await withConnection((conn) => sendKey(conn, key));
+ await showHUD(HUD_LABELS[key] ?? `Pressed ${key}`);
+}
+
+async function openApp(appName: string): Promise {
+ const cached = await loadCachedApps();
+ const resolved = resolveAppName(appName, cached?.apps ?? {});
+ if (!resolved) {
+ await showHUD(`❓ Don't know the app “${appName}”. Try the Launch Apple TV App command`);
+ return;
+ }
+ await withConnection((conn) => launchApp(conn, resolved.bundleId));
+ await showHUD(`📺 Opened ${resolved.name}`);
+}
+
+async function playTitle(title: string, appName?: string): Promise {
+ const result = await playContent(title, appName);
+ await showHUD(`${result.ok ? "▶️" : "❓"} ${result.message}`);
+}
+
+export default async function Ask(props: LaunchProps<{ arguments: { query: string } }>) {
+ const query = props.arguments.query.trim().toLowerCase();
+
+ try {
+ // 1. Direct key phrases ("pause", "volume up", …)
+ const key = KEY_PHRASES[query];
+ if (key) {
+ await pressKey(key);
+ return;
+ }
+
+ // 2. Power + gestures
+ if (/^(sleep|turn off|off)$/.test(query)) {
+ await withConnection(sleepDevice);
+ await showHUD("😴 Apple TV going to sleep");
+ return;
+ }
+ if (/^(wake|wake up|turn on|on)$/.test(query)) {
+ await withConnection(wakeDevice);
+ await showHUD("👋 Waking Apple TV");
+ return;
+ }
+ if (/^(app switcher|switcher|multitask)$/.test(query)) {
+ await withConnection(appSwitcher);
+ await showHUD("🗂 App Switcher");
+ return;
+ }
+ if (/^control center$/.test(query)) {
+ await withConnection(controlCenter);
+ await showHUD("🎛 Control Center");
+ return;
+ }
+ if (/^(context menu|hold select|long press)$/.test(query)) {
+ await withConnection(longPressSelect);
+ await showHUD("📋 Context Menu");
+ return;
+ }
+ if (/^screensaver$/.test(query)) {
+ await withConnection(startScreensaver);
+ await showHUD("🌌 Screensaver");
+ return;
+ }
+ const skipMatch = query.match(/^skip(?:\s+(back|backward|forward))?(?:\s+(\d+))?$/);
+ if (skipMatch) {
+ const seconds = Number(skipMatch[2] ?? 10) * (skipMatch[1]?.startsWith("back") ? -1 : 1);
+ await withConnection((conn) => skipBy(conn, seconds));
+ await showHUD(`${seconds > 0 ? "⏩" : "⏪"} ${Math.abs(seconds)}s`);
+ return;
+ }
+ const typeMatch = query.match(/^type\s+(.+)$/);
+ if (typeMatch) {
+ await withConnection((conn) => setText(conn, typeMatch[1]));
+ await showHUD("⌨️ Sent text to Apple TV");
+ return;
+ }
+
+ // 3. "open/launch ": URLs deep-link directly
+ const openMatch = query.match(/^(?:open|launch|start|go to)\s+(.+)$/);
+ if (openMatch) {
+ const target = openMatch[1].trim();
+ if (target.includes("://")) {
+ await withConnection((conn) => launchApp(conn, target));
+ await showHUD(`🔗 Sent ${target.split("/")[0]} link to Apple TV`);
+ return;
+ }
+ await openApp(target);
+ return;
+ }
+
+ // 4. "play/watch [on ]"
+ const playMatch = query.match(/^(?:play|watch)\s+(.+?)(?:\s+on\s+([a-z0-9+ ]+))?$/);
+ if (playMatch) {
+ await playTitle(playMatch[1], playMatch[2]);
+ return;
+ }
+
+ // 5. Maybe it's just an app name ("netflix")
+ if (resolveAppName(query)) {
+ await openApp(query);
+ return;
+ }
+
+ await showHUD(`❓ Try “pause”, “open Netflix”, or “play on ”`);
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/back.ts b/extensions/apple-tv-remote/src/back.ts
new file mode 100644
index 00000000000..8debfeca45f
--- /dev/null
+++ b/extensions/apple-tv-remote/src/back.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Menu));
+ await showHUD("↩️ Back");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/context-menu.ts b/extensions/apple-tv-remote/src/context-menu.ts
new file mode 100644
index 00000000000..12ce395d159
--- /dev/null
+++ b/extensions/apple-tv-remote/src/context-menu.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { longPressSelect } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => longPressSelect(conn));
+ await showHUD("📋 Context Menu");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/control-center.ts b/extensions/apple-tv-remote/src/control-center.ts
new file mode 100644
index 00000000000..45dae0843b9
--- /dev/null
+++ b/extensions/apple-tv-remote/src/control-center.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { controlCenter } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => controlCenter(conn));
+ await showHUD("🎛 Control Center");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/down.ts b/extensions/apple-tv-remote/src/down.ts
new file mode 100644
index 00000000000..0c52217407d
--- /dev/null
+++ b/extensions/apple-tv-remote/src/down.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Down));
+ await showHUD("⬇️ Down");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/home.ts b/extensions/apple-tv-remote/src/home.ts
new file mode 100644
index 00000000000..fb43a27dded
--- /dev/null
+++ b/extensions/apple-tv-remote/src/home.ts
@@ -0,0 +1,15 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Home(): Promise {
+ try {
+ await withConnection(async (conn) => {
+ await sendKey(conn, RemoteKey.Home);
+ });
+ await showHUD("🏠 Home");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/launch-app.tsx b/extensions/apple-tv-remote/src/launch-app.tsx
new file mode 100644
index 00000000000..866a1050a7d
--- /dev/null
+++ b/extensions/apple-tv-remote/src/launch-app.tsx
@@ -0,0 +1,108 @@
+import { Action, ActionPanel, Grid, Icon, LaunchType, popToRoot, showHUD } from "@raycast/api";
+import { createDeeplink, useCachedPromise } from "@raycast/utils";
+import { useEffect, useRef, useState } from "react";
+import { launchApp, listApps } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { KNOWN_APPS, loadCachedApps, saveCachedApps } from "./lib/deep-links";
+import { showErrorToast } from "./lib/errors";
+
+interface AppEntry {
+ bundleId: string;
+ name: string;
+}
+
+function recordToEntries(apps: Record): AppEntry[] {
+ return Object.entries(apps)
+ .map(([bundleId, name]) => ({ bundleId, name }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+}
+
+const FALLBACK_APPS: AppEntry[] = KNOWN_APPS.map((app) => ({ bundleId: app.bundleId, name: app.name })).sort((a, b) =>
+ a.name.localeCompare(b.name),
+);
+
+export default function LaunchAppCommand() {
+ // Seed instantly from cache so the grid renders without waiting on the device.
+ const cached = useCachedPromise(loadCachedApps, [], { keepPreviousData: true });
+ const [apps, setApps] = useState(null);
+ const [isRefreshing, setIsRefreshing] = useState(true);
+ const erroredRef = useRef(false);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function refresh() {
+ try {
+ const live = await withConnection((conn) => listApps(conn));
+ if (cancelled) return;
+ setApps(recordToEntries(live));
+ await saveCachedApps(live);
+ } catch (error) {
+ if (cancelled) return;
+ // Fall back to the known-app list and surface the failure exactly once.
+ setApps((prev) => prev ?? FALLBACK_APPS);
+ if (!erroredRef.current) {
+ erroredRef.current = true;
+ await showErrorToast(error);
+ }
+ } finally {
+ if (!cancelled) setIsRefreshing(false);
+ }
+ }
+
+ void refresh();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ async function open(entry: AppEntry) {
+ try {
+ await withConnection((conn) => launchApp(conn, entry.bundleId));
+ await showHUD(`Opened ${entry.name}`);
+ await popToRoot();
+ } catch (error) {
+ await showErrorToast(error);
+ }
+ }
+
+ // Prefer live results, then cached, then the static fallback.
+ const cachedEntries = cached.data ? recordToEntries(cached.data.apps) : undefined;
+ const items = apps ?? cachedEntries ?? FALLBACK_APPS;
+ const isLoading = (cached.isLoading || isRefreshing) && apps === null;
+
+ return (
+
+
+ {items.map((entry) => (
+
+ open(entry)} />
+ {/* Saved quicklinks accept their own global hotkey/alias, one keystroke to any app. */}
+
+
+ }
+ />
+ ))}
+
+ );
+}
diff --git a/extensions/apple-tv-remote/src/left.ts b/extensions/apple-tv-remote/src/left.ts
new file mode 100644
index 00000000000..a7ca41bd473
--- /dev/null
+++ b/extensions/apple-tv-remote/src/left.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Left));
+ await showHUD("⬅️ Left");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/lib/companion-extras.ts b/extensions/apple-tv-remote/src/lib/companion-extras.ts
new file mode 100644
index 00000000000..652f24d8a29
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/companion-extras.ts
@@ -0,0 +1,99 @@
+/**
+ * Companion-protocol features that @bharper/atv-js doesn't expose yet:
+ * app launching, installed-app listing, and sleep/wake. Payloads are ported
+ * from pyatv's reference implementation (pyatv/protocols/companion/api.py)
+ * and sent over the library's public `CompanionProtocol.sendCommand`, which
+ * handles request/response correlation on the encrypted session.
+ *
+ * Worth upstreaming to https://github.com/bsharper/atvjs.
+ */
+import { AppleTVConnection, HidCommand, RemoteKey, sendKeyDown, sendKeyUp, sendKey } from "@bharper/atv-js";
+
+/** pyatv `is_url_or_scheme`: URLs and custom schemes deep-link, bundle IDs launch. */
+function isUrlOrScheme(value: string): boolean {
+ return value.includes("://");
+}
+
+/**
+ * Launch an app by bundle ID (e.g. `com.netflix.Netflix`) or deep-link a URL
+ * (e.g. `https://www.netflix.com/title/80234304`).
+ */
+export async function launchApp(conn: AppleTVConnection, bundleIdOrUrl: string): Promise {
+ const key = isUrlOrScheme(bundleIdOrUrl) ? "_urlS" : "_bundleID";
+ await conn.protocol.sendCommand("_launchApp", { [key]: bundleIdOrUrl });
+}
+
+/** Installed apps as a map of bundle ID → display name. */
+export async function listApps(conn: AppleTVConnection): Promise> {
+ const response = await conn.protocol.sendCommand("FetchLaunchableApplicationsEvent", {});
+ return (response._c ?? {}) as Record;
+}
+
+/**
+ * Fire a HID command as a full button press. pyatv's sleep example sends only
+ * the button-up event, but its HID helper presses down+up like a real button,
+ * so we match the helper.
+ */
+async function pressHid(conn: AppleTVConnection, command: HidCommand): Promise {
+ await conn.protocol.sendCommand("_hidC", { _hBtS: 1, _hidC: command });
+ await conn.protocol.sendCommand("_hidC", { _hBtS: 2, _hidC: command });
+}
+
+export async function sleepDevice(conn: AppleTVConnection): Promise {
+ await pressHid(conn, HidCommand.Sleep);
+}
+
+export async function wakeDevice(conn: AppleTVConnection): Promise {
+ await pressHid(conn, HidCommand.Wake);
+}
+
+const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+/**
+ * Long-press a key (button down → 1s → up). tvOS distinguishes tap from hold
+ * at the ~1s boundary. This is how context menus open (long-press select)
+ * and how pyatv implements every "hold" action.
+ */
+export async function longPress(conn: AppleTVConnection, key: RemoteKey, holdMs = 1000): Promise {
+ await sendKeyDown(conn.protocol, key);
+ await delay(holdMs);
+ await sendKeyUp(conn.protocol, key);
+}
+
+/** Open the context menu for the focused item (physical remote: hold the clickpad). */
+export async function longPressSelect(conn: AppleTVConnection): Promise {
+ await longPress(conn, RemoteKey.Select);
+}
+
+/** App switcher (physical remote: double-press the TV button). */
+export async function appSwitcher(conn: AppleTVConnection): Promise {
+ await sendKey(conn, RemoteKey.Home);
+ await delay(150);
+ await sendKey(conn, RemoteKey.Home);
+}
+
+/**
+ * Open Control Center. Counter-intuitively this is NOT a Home-hold over the
+ * protocol, pyatv sends a single HID PageDown (19).
+ */
+export async function controlCenter(conn: AppleTVConnection): Promise {
+ await pressHid(conn, HidCommand.PageDown);
+}
+
+/** Start the screensaver. Known-flaky upstream (pyatv #2139), callers should tolerate failure. */
+export async function startScreensaver(conn: AppleTVConnection): Promise {
+ await pressHid(conn, HidCommand.Screensaver);
+}
+
+/**
+ * Skip within the current media by N seconds (negative = backward) via the
+ * MediaControl channel, the seconds-based path pyatv uses; the library's
+ * key-press mapping for the skip keys is incomplete upstream.
+ */
+export async function skipBy(conn: AppleTVConnection, seconds: number): Promise {
+ // The library's OPACK integer packer rejects negative integers, and pyatv
+ // sends _skpS as a float anyway, nudging the value off the integer grid
+ // routes it through the encoder's float64 branch without reaching into the
+ // library's internals. (Proper fix upstreamed in bsharper/atvjs#1.)
+ await conn.protocol.sendCommand("_mcc", { _mcc: 7, _skpS: seconds + 1e-9 });
+}
diff --git a/extensions/apple-tv-remote/src/lib/connection.ts b/extensions/apple-tv-remote/src/lib/connection.ts
new file mode 100644
index 00000000000..d6ead9f15cb
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/connection.ts
@@ -0,0 +1,62 @@
+import { getPreferenceValues } from "@raycast/api";
+import { AppleTVConnection, AppleTVDevice, connect, disconnect } from "@bharper/atv-js";
+import { loadSelectedDevice, saveSelectedDevice } from "./devices";
+import { loadCredentials } from "./credentials";
+import { UnreachableError } from "./errors";
+
+function connectTimeoutMs(): number {
+ const { connectTimeout } = getPreferenceValues();
+ const parsed = Number(connectTimeout);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 8000;
+}
+
+function withTimeout(promise: Promise, ms: number, onTimeout: () => Error): Promise {
+ let timer: NodeJS.Timeout;
+ const timeout = new Promise((_, reject) => {
+ timer = setTimeout(() => reject(onTimeout()), ms);
+ });
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)) as Promise;
+}
+
+/**
+ * Open a connection to the paired Apple TV (TCP + pair-verify + Companion
+ * session init). Caller owns the connection and must call `disconnect`.
+ */
+export async function openConnection(): Promise {
+ const device = await loadSelectedDevice();
+ const credentials = await loadCredentials(device.identifier);
+
+ // If `connect` outruns the timeout we stop awaiting it, but it may still
+ // resolve later with a live socket. Track that and dispose the orphan so a
+ // slow-but-eventually-successful connect can't leak a connection.
+ const pending = connect(device, credentials);
+ let timedOut = false;
+ pending.then((late) => timedOut && disconnect(late)).catch(() => {});
+
+ const conn = await withTimeout(pending, connectTimeoutMs(), () => {
+ timedOut = true;
+ return new UnreachableError(device.name);
+ });
+
+ // connect() re-discovers the endpoint if the saved address/port went stale
+ // (e.g. a new DHCP lease), persist the corrected record for next time.
+ if (!conn.usedCredentialsMatchProvided) {
+ await saveSelectedDevice(conn.device satisfies AppleTVDevice);
+ }
+
+ return conn;
+}
+
+/**
+ * Run a single action against the Apple TV with a short-lived connection.
+ * Used by no-view commands, the menu bar, and AI tools, the persistent-
+ * connection path for the remote view lives in `remote.tsx` itself.
+ */
+export async function withConnection(fn: (conn: AppleTVConnection) => Promise): Promise {
+ const conn = await openConnection();
+ try {
+ return await fn(conn);
+ } finally {
+ disconnect(conn);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/lib/credentials.ts b/extensions/apple-tv-remote/src/lib/credentials.ts
new file mode 100644
index 00000000000..6c3e6f17279
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/credentials.ts
@@ -0,0 +1,20 @@
+import { LocalStorage } from "@raycast/api";
+import { Credentials } from "@bharper/atv-js";
+import { NotPairedError } from "./errors";
+
+// Pairing credentials are machine-generated key material (not user-entered
+// secrets), so they live in Raycast's encrypted LocalStorage database rather
+// than extension preferences. Keychain access is intentionally not used.
+const credsKey = (deviceId: string) => `atv:creds:${deviceId}`;
+
+export async function saveCredentials(deviceId: string, creds: Credentials): Promise {
+ await LocalStorage.setItem(credsKey(deviceId), JSON.stringify(creds));
+}
+
+export async function loadCredentials(deviceId: string): Promise {
+ const raw = await LocalStorage.getItem(credsKey(deviceId));
+ if (!raw) {
+ throw new NotPairedError();
+ }
+ return JSON.parse(raw) as Credentials;
+}
diff --git a/extensions/apple-tv-remote/src/lib/deep-links.ts b/extensions/apple-tv-remote/src/lib/deep-links.ts
new file mode 100644
index 00000000000..5e242958054
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/deep-links.ts
@@ -0,0 +1,118 @@
+import { LocalStorage } from "@raycast/api";
+
+/**
+ * Well-known tvOS streaming apps. Bundle launches always work; `titleUrl`
+ * deep links open a specific show/movie where the app registers the pattern
+ * (confirmed working in pyatv's docs for Netflix, Disney+, Max, Apple TV).
+ */
+export interface KnownApp {
+ name: string;
+ bundleId: string;
+ aliases: string[];
+ /** Build a deep link from a service-specific content ID, when supported. */
+ titleUrl?: (contentId: string) => string;
+}
+
+export const KNOWN_APPS: KnownApp[] = [
+ {
+ name: "Netflix",
+ bundleId: "com.netflix.Netflix",
+ aliases: ["netflix"],
+ titleUrl: (id) => `https://www.netflix.com/title/${id}`,
+ },
+ {
+ name: "Disney+",
+ bundleId: "com.disney.disneyplus",
+ aliases: ["disney", "disney+", "disney plus", "disneyplus"],
+ },
+ {
+ name: "Max",
+ bundleId: "com.wbd.stream",
+ aliases: ["max", "hbo", "hbo max"],
+ },
+ {
+ name: "Apple TV",
+ bundleId: "com.apple.TVWatchList",
+ aliases: ["apple tv", "apple tv+", "tv app", "apple tv plus"],
+ titleUrl: (id) => `https://tv.apple.com/show/${id}?action=play`,
+ },
+ {
+ name: "YouTube",
+ bundleId: "com.google.ios.youtube",
+ aliases: ["youtube", "yt"],
+ },
+ {
+ name: "Hulu",
+ bundleId: "com.hulu.plus",
+ aliases: ["hulu"],
+ },
+ {
+ name: "Prime Video",
+ bundleId: "com.amazon.aiv.AIVApp",
+ aliases: ["prime", "prime video", "amazon", "amazon prime"],
+ },
+ {
+ name: "Spotify",
+ bundleId: "com.spotify.client",
+ aliases: ["spotify"],
+ },
+ {
+ name: "Plex",
+ bundleId: "com.plexapp.plex",
+ aliases: ["plex"],
+ },
+ {
+ name: "Settings",
+ bundleId: "com.apple.TVSettings",
+ aliases: ["settings"],
+ },
+];
+
+/** Match a user-supplied app name against the known apps and the live app list. */
+export function resolveAppName(
+ query: string,
+ installed: Record = {},
+): { bundleId: string; name: string } | null {
+ const q = query.trim().toLowerCase();
+ if (!q) return null;
+
+ // Exact/alias match against the curated map first (handles "hbo" → Max etc.)
+ for (const app of KNOWN_APPS) {
+ if (app.name.toLowerCase() === q || app.aliases.includes(q)) {
+ return { bundleId: app.bundleId, name: app.name };
+ }
+ }
+
+ // Then fuzzy match against what's actually installed on the device.
+ for (const [bundleId, name] of Object.entries(installed)) {
+ const n = name.toLowerCase();
+ if (n === q || n.includes(q) || q.includes(n)) {
+ return { bundleId, name };
+ }
+ }
+
+ // Finally substring match the curated map.
+ for (const app of KNOWN_APPS) {
+ if (app.name.toLowerCase().includes(q) || app.aliases.some((a) => a.includes(q))) {
+ return { bundleId: app.bundleId, name: app.name };
+ }
+ }
+
+ return null;
+}
+
+const APP_CACHE_KEY = "atv:apps";
+
+export interface CachedApps {
+ apps: Record;
+ fetchedAt: number;
+}
+
+export async function loadCachedApps(): Promise {
+ const raw = await LocalStorage.getItem(APP_CACHE_KEY);
+ return raw ? (JSON.parse(raw) as CachedApps) : null;
+}
+
+export async function saveCachedApps(apps: Record): Promise {
+ await LocalStorage.setItem(APP_CACHE_KEY, JSON.stringify({ apps, fetchedAt: Date.now() } satisfies CachedApps));
+}
diff --git a/extensions/apple-tv-remote/src/lib/devices.ts b/extensions/apple-tv-remote/src/lib/devices.ts
new file mode 100644
index 00000000000..b60b5b61a6f
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/devices.ts
@@ -0,0 +1,48 @@
+import { LocalStorage } from "@raycast/api";
+import { AppleTVDevice, scan } from "@bharper/atv-js";
+import { NotPairedError } from "./errors";
+
+const SELECTED_DEVICE_KEY = "atv:device:selected";
+
+/** Scan the local network for Apple TVs (mDNS, _companion-link._tcp). */
+export async function scanForDevices(timeoutMs = 5000): Promise {
+ return scan(timeoutMs, true);
+}
+
+/** Persist the device the user paired with so commands never need to re-scan. */
+export async function saveSelectedDevice(device: AppleTVDevice): Promise {
+ await LocalStorage.setItem(SELECTED_DEVICE_KEY, JSON.stringify(device));
+}
+
+export async function loadSelectedDevice(): Promise {
+ const raw = await LocalStorage.getItem(SELECTED_DEVICE_KEY);
+ if (!raw) {
+ throw new NotPairedError();
+ }
+ return JSON.parse(raw) as AppleTVDevice;
+}
+
+export async function getSelectedDeviceOrNull(): Promise {
+ try {
+ return await loadSelectedDevice();
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Build a device record from a manually entered IP address, for networks
+ * where mDNS discovery is blocked. The Companion port varies per device
+ * (49152–49155), so it's user-overridable.
+ */
+export function deviceFromManualEntry(name: string, address: string, port = 49152): AppleTVDevice {
+ return {
+ name: name || "Apple TV",
+ address,
+ port,
+ airplayPort: 7000,
+ identifier: `manual-${address}`,
+ model: "unknown",
+ properties: {},
+ };
+}
diff --git a/extensions/apple-tv-remote/src/lib/errors.ts b/extensions/apple-tv-remote/src/lib/errors.ts
new file mode 100644
index 00000000000..c4c9c023196
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/errors.ts
@@ -0,0 +1,64 @@
+import { Toast, showToast, launchCommand, LaunchType } from "@raycast/api";
+
+/** No Apple TV has been set up yet (no stored device/credentials). */
+export class NotPairedError extends Error {
+ constructor() {
+ super("No Apple TV is set up yet. Run “Set up Apple TV” to pair one.");
+ this.name = "NotPairedError";
+ }
+}
+
+/** The Apple TV could not be reached (offline, network change, timeout). */
+export class UnreachableError extends Error {
+ constructor(deviceName: string) {
+ super(`Could not reach “${deviceName}”. Is it powered on and on the same network?`);
+ this.name = "UnreachableError";
+ }
+}
+
+async function openSetup() {
+ try {
+ await launchCommand({ name: "setup", type: LaunchType.UserInitiated });
+ } catch (error) {
+ await showToast({
+ style: Toast.Style.Failure,
+ title: "Couldn't Open Setup",
+ message: error instanceof Error ? error.message : String(error),
+ });
+ }
+}
+
+/**
+ * Show a failure toast appropriate for the error. Pairing problems get a
+ * "Set Up Apple TV" action so the user can fix them in one step.
+ */
+export async function showErrorToast(error: unknown): Promise {
+ if (error instanceof NotPairedError) {
+ await showToast({
+ style: Toast.Style.Failure,
+ title: "No Apple TV Set Up",
+ message: error.message,
+ primaryAction: { title: "Set up Apple TV", onAction: openSetup },
+ });
+ return;
+ }
+
+ if (error instanceof UnreachableError) {
+ await showToast({
+ style: Toast.Style.Failure,
+ title: "Apple TV Unreachable",
+ message: error.message,
+ });
+ return;
+ }
+
+ const message = error instanceof Error ? error.message : String(error);
+ // Pair-verify failures mean the stored credentials are no longer valid.
+ const looksLikeAuth = /pair|verify|auth|credential/i.test(message);
+ await showToast({
+ style: Toast.Style.Failure,
+ title: looksLikeAuth ? "Pairing Expired" : "Apple TV Error",
+ message: looksLikeAuth ? "Re-pair with your Apple TV to continue." : message,
+ primaryAction: looksLikeAuth ? { title: "Set up Apple TV", onAction: openSetup } : undefined,
+ });
+}
diff --git a/extensions/apple-tv-remote/src/lib/justwatch.ts b/extensions/apple-tv-remote/src/lib/justwatch.ts
new file mode 100644
index 00000000000..f45e1f52a74
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/justwatch.ts
@@ -0,0 +1,166 @@
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+import { LocalStorage, getPreferenceValues } from "@raycast/api";
+
+const execFileAsync = promisify(execFile);
+
+/**
+ * Deterministic title → streaming deep-link resolution via JustWatch's GraphQL
+ * API (the same backend justwatch.com uses; keyless, non-commercial use).
+ * This replaces LLM-guessed title IDs with real per-provider URLs.
+ * Results are cached for 24h to keep API usage polite.
+ */
+
+const GRAPHQL_URL = "https://apis.justwatch.com/graphql";
+const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
+
+const SEARCH_QUERY = `query GetSearchTitles($searchTitlesFilter: TitleFilter!, $country: Country!, $language: Language!, $first: Int!, $filter: OfferFilter!) {
+ popularTitles(country: $country, filter: $searchTitlesFilter, first: $first, sortBy: POPULAR, sortRandomSeed: 0) {
+ edges {
+ node {
+ objectType
+ content(country: $country, language: $language) {
+ title
+ originalReleaseYear
+ }
+ ... on MovieOrShowOrSeason {
+ offers(country: $country, platform: WEB, filter: $filter) {
+ monetizationType
+ standardWebURL
+ package {
+ technicalName
+ shortName
+ clearName
+ }
+ }
+ }
+ }
+ }
+ }
+}`;
+
+export interface StreamingOffer {
+ url: string;
+ monetizationType: string;
+ provider: { technicalName: string; shortName: string; clearName: string };
+}
+
+export interface ResolvedTitle {
+ title: string;
+ year: number | null;
+ type: string;
+ offers: StreamingOffer[];
+}
+
+let detectedCountry: string | null = null;
+
+/** Country from the extension preference, else auto-detected from the Mac's region setting. */
+async function country(): Promise {
+ const prefs = getPreferenceValues();
+ const fromPref = (prefs.country ?? "").trim().toUpperCase();
+ if (/^[A-Z]{2}$/.test(fromPref)) return fromPref;
+
+ if (!detectedCountry) {
+ try {
+ // macOS region, e.g. "en_AU" → AU
+ const { stdout } = await execFileAsync("/usr/bin/defaults", ["read", "-g", "AppleLocale"]);
+ detectedCountry = stdout.trim().match(/_([A-Z]{2})\b/)?.[1] ?? "US";
+ } catch {
+ detectedCountry = "US";
+ }
+ }
+ return detectedCountry;
+}
+
+export async function searchTitle(query: string): Promise {
+ const region = await country();
+ const cacheKey = `atv:jw:${region}:${query.toLowerCase()}`;
+ const cached = await LocalStorage.getItem(cacheKey);
+ if (cached) {
+ const { value, at } = JSON.parse(cached) as { value: ResolvedTitle | null; at: number };
+ if (Date.now() - at < CACHE_TTL_MS) return value;
+ }
+
+ const response = await fetch(GRAPHQL_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ operationName: "GetSearchTitles",
+ variables: {
+ first: 3,
+ searchTitlesFilter: { searchQuery: query },
+ country: region,
+ language: "en",
+ filter: { bestOnly: true },
+ },
+ query: SEARCH_QUERY,
+ }),
+ });
+ if (!response.ok) throw new Error(`JustWatch lookup failed (HTTP ${response.status})`);
+
+ const json = (await response.json()) as {
+ data?: {
+ popularTitles?: {
+ edges?: {
+ node: {
+ objectType: string;
+ content?: { title?: string; originalReleaseYear?: number };
+ offers?: {
+ monetizationType?: string;
+ standardWebURL?: string;
+ package?: { technicalName?: string; shortName?: string; clearName?: string };
+ }[];
+ };
+ }[];
+ };
+ };
+ };
+
+ const node = json.data?.popularTitles?.edges?.[0]?.node;
+ const value: ResolvedTitle | null = node
+ ? {
+ title: node.content?.title ?? query,
+ year: node.content?.originalReleaseYear ?? null,
+ type: node.objectType,
+ offers: (node.offers ?? [])
+ .filter((o) => o.standardWebURL && o.package?.technicalName)
+ .map((o) => ({
+ url: o.standardWebURL as string,
+ monetizationType: o.monetizationType ?? "UNKNOWN",
+ provider: {
+ technicalName: o.package?.technicalName ?? "",
+ shortName: o.package?.shortName ?? "",
+ clearName: o.package?.clearName ?? "",
+ },
+ })),
+ }
+ : null;
+
+ await LocalStorage.setItem(cacheKey, JSON.stringify({ value, at: Date.now() }));
+ return value;
+}
+
+/** Whether an offer belongs to the provider the user explicitly asked for. */
+export function offerMatchesHint(offer: StreamingOffer, providerHint: string): boolean {
+ const hint = providerHint.trim().toLowerCase();
+ return (
+ offer.provider.technicalName.includes(hint.replace(/[^a-z0-9]/g, "")) ||
+ offer.provider.clearName.toLowerCase().includes(hint) ||
+ hint.includes(offer.provider.clearName.toLowerCase())
+ );
+}
+
+/**
+ * Pick the best offer: an explicit provider request wins, then subscription
+ * (FLATRATE) offers, then anything else.
+ */
+export function pickOffer(resolved: ResolvedTitle, providerHint?: string): StreamingOffer | null {
+ if (resolved.offers.length === 0) return null;
+
+ if (providerHint) {
+ const match = resolved.offers.find((o) => offerMatchesHint(o, providerHint));
+ if (match) return match;
+ }
+
+ return resolved.offers.find((o) => o.monetizationType === "FLATRATE") ?? resolved.offers[0];
+}
diff --git a/extensions/apple-tv-remote/src/lib/play-flow.ts b/extensions/apple-tv-remote/src/lib/play-flow.ts
new file mode 100644
index 00000000000..2467d55ce97
--- /dev/null
+++ b/extensions/apple-tv-remote/src/lib/play-flow.ts
@@ -0,0 +1,205 @@
+import { AppleTVConnection, RemoteKey, getKeyboardFocusState, sendKey, setText } from "@bharper/atv-js";
+import { getPreferenceValues } from "@raycast/api";
+import { withConnection } from "./connection";
+import { launchApp } from "./companion-extras";
+import { resolveAppName } from "./deep-links";
+import { offerMatchesHint, pickOffer, searchTitle } from "./justwatch";
+
+/**
+ * The "play [on ]" flow, shared by the AI tool and the Ask command.
+ *
+ * Strategy, in order of reliability:
+ * 1. JustWatch-resolved deep link (real per-provider URL, no guessed IDs),
+ * works for Apple TV+, Disney+, Max, and most others.
+ * 2. Netflix (whose tvOS deep links broke in Sept 2025) and unresolvable
+ * titles fall back to tvOS universal Search: launch the system Search app,
+ * wait for the on-screen keyboard (a real Companion text-input session,
+ * verifiable!), and type the title. Selecting a result deep-links natively.
+ * 3. Bare app launch as the last resort.
+ */
+
+const TV_SEARCH_BUNDLE = "com.apple.TVSearch";
+
+/** Providers whose web URLs don't deep-link on tvOS, route via universal search. */
+const BROKEN_DEEP_LINK_PROVIDERS = new Set(["netflix", "netflixbasicwithads"]);
+
+const PROVIDER_BUNDLES: Record = {
+ netflix: "com.netflix.Netflix",
+ netflixbasicwithads: "com.netflix.Netflix",
+ disneyplus: "com.disney.disneyplus",
+ max: "com.wbd.stream",
+ hbomax: "com.wbd.stream",
+ appletvplus: "com.apple.TVWatchList",
+ itunes: "com.apple.TVWatchList",
+ hulu: "com.hulu.plus",
+ amazonprimevideo: "com.amazon.aiv.AIVApp",
+ amazonprime: "com.amazon.aiv.AIVApp",
+ youtube: "com.google.ios.youtube",
+};
+
+/** Per-app URL fixups for tvOS routing quirks. */
+function adaptUrlForTvos(url: string, technicalName: string): string {
+ if (technicalName === "youtube") {
+ // The scheme form routes reliably on tvOS; plain https is flaky.
+ const id = url.match(/[?&]v=([\w-]+)/)?.[1];
+ if (id) return `youtube://www.youtube.com/watch?v=${id}`;
+ }
+ if (technicalName === "appletvplus" || technicalName === "itunes") {
+ return url.includes("?") ? `${url}&action=play` : `${url}?action=play`;
+ }
+ return url;
+}
+
+const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+// tvOS coalesces rapid keypresses into long-presses (pyatv #792), keep gaps generous.
+const KEY_GAP_MS = 800;
+
+async function keyboardFocused(conn: AppleTVConnection): Promise {
+ try {
+ return await getKeyboardFocusState(conn);
+ } catch {
+ return false;
+ }
+}
+
+export type SearchAutomation = "type" | "open" | "play";
+
+type SearchStage = "failed" | "typed" | "opened" | "played";
+
+/**
+ * Launch tvOS universal Search, type the title, then walk the focus-state
+ * machine toward playback. The keyboard-focus flag is the only feedback
+ * channel, and it gives us two verifiable transitions:
+ * - Down flips focus → false: we verifiably left the keyboard into results.
+ * (If it doesn't flip, that's the known tvOS single/zero-result quirk, bail
+ * and leave the typed query on screen rather than pressing blind.)
+ * - Select flips focus back → true: we hit a search *suggestion*, not a
+ * poster, the query was refined, so descend again and re-select.
+ * After opening the top result, one more Select hits the detail page's
+ * default-focused Play/Open button. Playback itself isn't verifiable without
+ * the MRP channel, so messaging stays honest about that.
+ */
+async function searchAndPlay(
+ conn: AppleTVConnection,
+ title: string,
+ automation: SearchAutomation,
+): Promise {
+ await launchApp(conn, TV_SEARCH_BUNDLE);
+
+ let typed = false;
+ for (let attempt = 0; attempt < 16; attempt++) {
+ await delay(500);
+ if (await keyboardFocused(conn)) {
+ await setText(conn, title);
+ typed = true;
+ break;
+ }
+ }
+ if (!typed) return "failed";
+ if (automation === "type") return "typed";
+
+ // Let live results populate (no "results ready" event exists).
+ await delay(2000);
+
+ // Leave the keyboard, verified by the focus flag flipping false.
+ let leftKeyboard = false;
+ for (let presses = 0; presses < 2; presses++) {
+ await sendKey(conn, RemoteKey.Down);
+ await delay(KEY_GAP_MS);
+ if (!(await keyboardFocused(conn))) {
+ leftKeyboard = true;
+ break;
+ }
+ }
+ if (!leftKeyboard) return "typed"; // single/zero-result quirk, don't press blind
+
+ // Open the focused item.
+ await sendKey(conn, RemoteKey.Select);
+ await delay(1500);
+
+ // If the keyboard came back, we selected a suggestion (query refined),
+ // descend into the refreshed results and open the top poster.
+ if (await keyboardFocused(conn)) {
+ await delay(800);
+ await sendKey(conn, RemoteKey.Down);
+ await delay(KEY_GAP_MS);
+ if (await keyboardFocused(conn)) return "typed";
+ await sendKey(conn, RemoteKey.Select);
+ await delay(1500);
+ }
+
+ // We're now on the title's canonical page. Stopping here is the reliable
+ // default: it lists every provider, and which "watch option" has default
+ // focus is invisible to us, a blind Play can start the wrong app.
+ if (automation !== "play") return "opened";
+
+ await delay(KEY_GAP_MS);
+ await sendKey(conn, RemoteKey.Select);
+ return "played";
+}
+
+export interface PlayResult {
+ ok: boolean;
+ message: string;
+}
+
+export async function playContent(title: string, appHint?: string): Promise {
+ // 1. Deterministic resolution (cached, keyless).
+ let resolvedTitle: Awaited> = null;
+ try {
+ resolvedTitle = await searchTitle(title);
+ } catch {
+ // offline or API hiccup, fall through to search/app-launch paths
+ }
+
+ const offer = resolvedTitle ? pickOffer(resolvedTitle, appHint) : null;
+ const displayTitle = resolvedTitle?.title ?? title;
+
+ // Never silently open a different service than the one the user named,
+ // if their provider has no offer in this region, the universal-search flow
+ // lets them decide on screen instead.
+ const honorsHint = !appHint || (offer !== null && offerMatchesHint(offer, appHint));
+
+ // 2. Direct deep link when the provider supports it on tvOS.
+ if (offer && honorsHint && !BROKEN_DEEP_LINK_PROVIDERS.has(offer.provider.technicalName)) {
+ const url = adaptUrlForTvos(offer.url, offer.provider.technicalName);
+ await withConnection((conn) => launchApp(conn, url));
+ return { ok: true, message: `Opening ${displayTitle} in ${offer.provider.clearName}` };
+ }
+
+ // 3. Universal Search flow (Netflix & friends, or unresolved titles).
+ const { searchAutomation } = getPreferenceValues();
+ const stage = await withConnection((conn) => searchAndPlay(conn, displayTitle, searchAutomation ?? "open"));
+ if (stage !== "failed") {
+ const where = offer
+ ? honorsHint
+ ? ` (it's on ${offer.provider.clearName})`
+ : ` (in your region it's on ${offer.provider.clearName})`
+ : "";
+ return {
+ ok: true,
+ message:
+ stage === "played"
+ ? `Pressed Play on the top result for “${displayTitle}”${where}. Check the TV; the default watch option may not be your preferred app.`
+ : stage === "opened"
+ ? `Opened “${displayTitle}”${where}. Pick your streaming app on the title page.`
+ : `Typed “${displayTitle}” into Apple TV Search${where}. Pick the result on screen.`,
+ };
+ }
+
+ // 4. Last resort: open the most plausible app (the user's named app wins).
+ const bundleFromOffer = offer && honorsHint ? PROVIDER_BUNDLES[offer.provider.technicalName] : undefined;
+ const fromHint = appHint ? resolveAppName(appHint) : null;
+ const bundleId = fromHint?.bundleId ?? bundleFromOffer;
+ if (bundleId) {
+ await withConnection((conn) => launchApp(conn, bundleId));
+ const appName = offer?.provider.clearName ?? fromHint?.name ?? "the app";
+ return { ok: true, message: `Opened ${appName}. Search for “${displayTitle}” there.` };
+ }
+
+ return {
+ ok: false,
+ message: `Couldn't find “${title}” or a matching app. Try “play ${title} on Netflix”.`,
+ };
+}
diff --git a/extensions/apple-tv-remote/src/menubar.tsx b/extensions/apple-tv-remote/src/menubar.tsx
new file mode 100644
index 00000000000..e8863773f8a
--- /dev/null
+++ b/extensions/apple-tv-remote/src/menubar.tsx
@@ -0,0 +1,92 @@
+import { Icon, LaunchType, MenuBarExtra, launchCommand } from "@raycast/api";
+import { usePromise } from "@raycast/utils";
+import { getSelectedDeviceOrNull } from "./lib/devices";
+import { loadCachedApps } from "./lib/deep-links";
+
+/**
+ * Menu-bar remote. Clicking an item closes the menu and Raycast unloads this
+ * command immediately, which would kill an in-flight Companion handshake. So
+ * every action is delegated to a background launch of the `ask` command,
+ * which gets its own process lifetime and completes reliably.
+ */
+
+const fire = (query: string) => () =>
+ void launchCommand({ name: "ask", type: LaunchType.Background, arguments: { query } }).catch(() => {});
+
+export default function Command() {
+ const { data: device, isLoading } = usePromise(getSelectedDeviceOrNull);
+ const { data: cachedApps } = usePromise(loadCachedApps);
+
+ const topApps = cachedApps ? Object.entries(cachedApps.apps).slice(0, 12) : [];
+
+ return (
+
+
+ void launchCommand({
+ name: device ? "remote" : "setup",
+ type: LaunchType.UserInitiated,
+ }).catch(() => {})
+ }
+ />
+
+
+
+
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+ {topApps.length > 0 && (
+
+
+ {topApps.map(([bundleId, name]) => (
+
+ ))}
+
+
+ )}
+
+
+ }
+ />
+
+
+ );
+}
diff --git a/extensions/apple-tv-remote/src/play-pause.ts b/extensions/apple-tv-remote/src/play-pause.ts
new file mode 100644
index 00000000000..d913932928b
--- /dev/null
+++ b/extensions/apple-tv-remote/src/play-pause.ts
@@ -0,0 +1,15 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function PlayPause(): Promise {
+ try {
+ await withConnection(async (conn) => {
+ await sendKey(conn, RemoteKey.PlayPause);
+ });
+ await showHUD("⏯ Play/Pause");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/remote.tsx b/extensions/apple-tv-remote/src/remote.tsx
new file mode 100644
index 00000000000..acbbb9bd22a
--- /dev/null
+++ b/extensions/apple-tv-remote/src/remote.tsx
@@ -0,0 +1,383 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Action, ActionPanel, Color, Form, Grid, Icon, Keyboard, Toast, showToast, useNavigation } from "@raycast/api";
+import { AppleTVConnection, RemoteKey, disconnect, onConnectionLost, sendKey, setText } from "@bharper/atv-js";
+import { openConnection } from "./lib/connection";
+import { NotPairedError, showErrorToast } from "./lib/errors";
+import { appSwitcher, controlCenter, longPressSelect, skipBy, startScreensaver } from "./lib/companion-extras";
+
+type Status = "connecting" | "connected" | "reconnecting" | "disconnected" | "not-paired";
+
+type DeviceAction = (conn: AppleTVConnection) => Promise;
+
+/**
+ * A visual Apple TV remote: a 3-column grid laid out like the physical remote,
+ * clickable with the mouse, holding ONE live Companion connection so every
+ * press is instant. Keyboard layers on top:
+ * - Bare keys via search interception: WASD/HJKL move, F select, Space ⏯.
+ * - ⌥-shortcuts for every action, regardless of selection.
+ */
+export default function Remote() {
+ const connRef = useRef(null);
+ // The connection attempt currently in flight, shared by every caller so a
+ // burst of presses during the handshake reuses one attempt instead of each
+ // opening — and leaking — its own connection.
+ const establishingRef = useRef | null>(null);
+ const mountedRef = useRef(true);
+ const [status, setStatus] = useState("connecting");
+ const [deviceName, setDeviceName] = useState("Apple TV");
+ const { push } = useNavigation();
+
+ // Open (or reopen) the single persistent connection. `openConnection` can take
+ // up to the connect timeout (~8s), during which `connRef.current` is null;
+ // without coalescing, every key pressed in that window would launch another
+ // `openConnection` and abandon the loser's socket. So concurrent callers share
+ // one in-flight promise, and stale connections are torn down deterministically.
+ const establish = useCallback(async (): Promise => {
+ if (establishingRef.current) return establishingRef.current;
+
+ const attempt = (async (): Promise => {
+ // Retire any existing connection up front (the ref is only non-null here
+ // on a manual reconnect over a live connection). Tearing it down *before*
+ // opening the replacement means no in-flight action keeps using a socket
+ // we're about to drop, and a failed reconnect can't leak the old one.
+ const previous = connRef.current;
+ connRef.current = null;
+ if (previous) disconnect(previous);
+
+ setStatus((s) => (s === "connected" ? "reconnecting" : "connecting"));
+ try {
+ const conn = await openConnection();
+ if (!mountedRef.current) {
+ // Unmounted mid-handshake: dispose the socket instead of leaking it.
+ disconnect(conn);
+ return null;
+ }
+ connRef.current = conn;
+ setDeviceName(conn.device.name);
+ setStatus("connected");
+ onConnectionLost(conn, () => {
+ // Only react to drops of the connection that's still active; a stale
+ // connection's callback (incl. the one our own disconnect triggers)
+ // must not clobber a newer live connection.
+ if (connRef.current === conn) {
+ connRef.current = null;
+ setStatus("disconnected");
+ }
+ });
+ return conn;
+ } catch (error) {
+ if (mountedRef.current) {
+ setStatus(error instanceof NotPairedError ? "not-paired" : "disconnected");
+ await showErrorToast(error);
+ }
+ return null;
+ } finally {
+ establishingRef.current = null;
+ }
+ })();
+
+ establishingRef.current = attempt;
+ return attempt;
+ }, []);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ void establish();
+ return () => {
+ mountedRef.current = false;
+ // Null the ref before disconnecting so the lost-callback's identity check
+ // fails and we don't setState on the unmounted component.
+ const conn = connRef.current;
+ connRef.current = null;
+ if (conn) disconnect(conn);
+ };
+ }, [establish]);
+
+ const run = useCallback(
+ async (action: DeviceAction) => {
+ const conn = connRef.current ?? (await establish());
+ if (!conn) return;
+ try {
+ await action(conn);
+ } catch (error) {
+ await showErrorToast(error);
+ }
+ },
+ [establish],
+ );
+
+ const press = useCallback((key: RemoteKey) => run((conn) => sendKey(conn, key)), [run]);
+
+ const pushTypeText = useCallback(() => {
+ push( );
+ }, [push]);
+
+ // Bare-key layer: typed characters are button presses.
+ const handleTyped = useCallback(
+ (text: string) => {
+ for (const ch of text.toLowerCase()) {
+ const map: Record void> = {
+ w: () => void press(RemoteKey.Up),
+ k: () => void press(RemoteKey.Up),
+ s: () => void press(RemoteKey.Down),
+ j: () => void press(RemoteKey.Down),
+ a: () => void press(RemoteKey.Left),
+ h: () => void press(RemoteKey.Left),
+ d: () => void press(RemoteKey.Right),
+ l: () => void press(RemoteKey.Right),
+ f: () => void press(RemoteKey.Select),
+ g: () => void press(RemoteKey.Select),
+ " ": () => void press(RemoteKey.PlayPause),
+ b: () => void press(RemoteKey.Menu),
+ q: () => void press(RemoteKey.Home),
+ v: () => void run(longPressSelect),
+ x: () => void run(appSwitcher),
+ c: () => void run(controlCenter),
+ "[": () => void press(RemoteKey.Previous),
+ "]": () => void press(RemoteKey.Next),
+ ",": () => void run((conn) => skipBy(conn, -10)),
+ ".": () => void run((conn) => skipBy(conn, 10)),
+ "-": () => void press(RemoteKey.VolumeDown),
+ "=": () => void press(RemoteKey.VolumeUp),
+ "+": () => void press(RemoteKey.VolumeUp),
+ t: () => pushTypeText(),
+ };
+ map[ch]?.();
+ }
+ },
+ [press, run, pushTypeText],
+ );
+
+ const statusLabel =
+ status === "connected"
+ ? "Connected"
+ : status === "connecting" || status === "reconnecting"
+ ? "Connecting…"
+ : status === "not-paired"
+ ? "Not Paired"
+ : "Disconnected";
+
+ // Shared shortcut actions available from every cell.
+ const sharedShortcuts = (
+ <>
+
+ press(RemoteKey.Up)}
+ />
+ press(RemoteKey.Down)}
+ />
+ press(RemoteKey.Left)}
+ />
+ press(RemoteKey.Right)}
+ />
+ press(RemoteKey.Select)}
+ />
+ press(RemoteKey.Menu)}
+ />
+
+
+ press(RemoteKey.PlayPause)}
+ />
+ run(startScreensaver)}
+ />
+
+
+ >
+ );
+
+ interface Cell {
+ id: string;
+ icon: Icon | "blank";
+ title: string;
+ tint?: Color;
+ action?: DeviceAction | "type-text";
+ }
+
+ const B = (id: string): Cell => ({ id, icon: "blank", title: "" });
+
+ const cells: Cell[] = [
+ // Row 1
+ B("b1"),
+ { id: "ctx", icon: Icon.BulletPoints, title: "Hold · V", action: longPressSelect },
+ {
+ id: "up",
+ icon: Icon.ChevronUp,
+ title: "Up · W",
+ tint: Color.PrimaryText,
+ action: (c) => sendKey(c, RemoteKey.Up),
+ },
+ { id: "switcher", icon: Icon.AppWindowGrid2x2, title: "Apps · X", action: appSwitcher },
+ B("b2"),
+ // Row 2
+ B("b3"),
+ {
+ id: "left",
+ icon: Icon.ChevronLeft,
+ title: "Left · A",
+ tint: Color.PrimaryText,
+ action: (c) => sendKey(c, RemoteKey.Left),
+ },
+ {
+ id: "select",
+ icon: Icon.CircleFilled,
+ title: "Select · F",
+ tint: Color.Blue,
+ action: (c) => sendKey(c, RemoteKey.Select),
+ },
+ {
+ id: "right",
+ icon: Icon.ChevronRight,
+ title: "Right · D",
+ tint: Color.PrimaryText,
+ action: (c) => sendKey(c, RemoteKey.Right),
+ },
+ B("b4"),
+ // Row 3
+ B("b5"),
+ { id: "back", icon: Icon.Undo, title: "Back · B", action: (c) => sendKey(c, RemoteKey.Menu) },
+ {
+ id: "down",
+ icon: Icon.ChevronDown,
+ title: "Down · S",
+ tint: Color.PrimaryText,
+ action: (c) => sendKey(c, RemoteKey.Down),
+ },
+ { id: "home", icon: Icon.House, title: "Home · Q", action: (c) => sendKey(c, RemoteKey.Home) },
+ B("b6"),
+ // Row 4
+ B("b7"),
+ { id: "skipback", icon: Icon.Rewind, title: "−10s · ,", action: (c) => skipBy(c, -10) },
+ {
+ id: "playpause",
+ icon: Icon.PlayFilled,
+ title: "Play · ␣",
+ tint: Color.Blue,
+ action: (c) => sendKey(c, RemoteKey.PlayPause),
+ },
+ { id: "skipfwd", icon: Icon.Forward, title: "+10s · .", action: (c) => skipBy(c, 10) },
+ B("b8"),
+ // Row 5
+ B("b9"),
+ { id: "prev", icon: Icon.RewindFilled, title: "Prev · [", action: (c) => sendKey(c, RemoteKey.Previous) },
+ { id: "cc", icon: Icon.Switch, title: "Control Center · C", action: controlCenter },
+ { id: "next", icon: Icon.ForwardFilled, title: "Next · ]", action: (c) => sendKey(c, RemoteKey.Next) },
+ B("b10"),
+ // Row 6
+ B("b11"),
+ B("b12"),
+ { id: "type", icon: Icon.Keyboard, title: "Type · T", action: "type-text" },
+ B("b13"),
+ B("b14"),
+ ];
+
+ return (
+
+
+ {cells.map((cell) => (
+
+ (cell.action === "type-text" ? pushTypeText() : run(cell.action as DeviceAction))}
+ />
+ {sharedShortcuts}
+
+ ) : undefined
+ }
+ />
+ ))}
+
+
+ );
+}
+
+function TypeTextForm({ connRef }: { connRef: React.MutableRefObject }) {
+ const { pop } = useNavigation();
+
+ return (
+
+ );
+}
diff --git a/extensions/apple-tv-remote/src/right.ts b/extensions/apple-tv-remote/src/right.ts
new file mode 100644
index 00000000000..9804a8b7919
--- /dev/null
+++ b/extensions/apple-tv-remote/src/right.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Right));
+ await showHUD("➡️ Right");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/screensaver.ts b/extensions/apple-tv-remote/src/screensaver.ts
new file mode 100644
index 00000000000..65397034b18
--- /dev/null
+++ b/extensions/apple-tv-remote/src/screensaver.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { startScreensaver } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => startScreensaver(conn));
+ await showHUD("🌌 Screensaver");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/select.ts b/extensions/apple-tv-remote/src/select.ts
new file mode 100644
index 00000000000..90fd9f99ace
--- /dev/null
+++ b/extensions/apple-tv-remote/src/select.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Select));
+ await showHUD("⏺ Select");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/setup.tsx b/extensions/apple-tv-remote/src/setup.tsx
new file mode 100644
index 00000000000..0a82f661fdc
--- /dev/null
+++ b/extensions/apple-tv-remote/src/setup.tsx
@@ -0,0 +1,186 @@
+import { useEffect, useState } from "react";
+import { Action, ActionPanel, Form, Icon, List, Toast, popToRoot, showToast, useNavigation } from "@raycast/api";
+import {
+ AppleTVDevice,
+ PairingSession,
+ finishCompanionPairing,
+ serializeCredentials,
+ startCompanionPairing,
+} from "@bharper/atv-js";
+import { deviceFromManualEntry, saveSelectedDevice, scanForDevices } from "./lib/devices";
+import { saveCredentials } from "./lib/credentials";
+
+/**
+ * Pairing flow: scan → pick a device → 4-digit PIN appears on the TV →
+ * enter it → credentials stored. Pairing is interactive by nature (the PIN
+ * is generated by the Apple TV), so it can't be a preference.
+ */
+export default function Setup() {
+ const [devices, setDevices] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const found = await scanForDevices(5000);
+ if (!cancelled) setDevices(found);
+ } catch (error) {
+ await showToast({
+ style: Toast.Style.Failure,
+ title: "Scan Failed",
+ message: error instanceof Error ? error.message : String(error),
+ });
+ } finally {
+ if (!cancelled) setIsLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return (
+
+
+ } />
+
+ }
+ />
+
+ {devices.map((device) => (
+
+ } />
+ } />
+
+ }
+ />
+ ))}
+
+
+ );
+}
+
+function ManualEntryForm() {
+ const { push } = useNavigation();
+
+ return (
+
+ );
+}
+
+function PairForm({ device }: { device: AppleTVDevice }) {
+ const [session, setSession] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const s = await startCompanionPairing(device);
+ if (!cancelled) setSession(s);
+ } catch (e) {
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [device]);
+
+ async function submit(values: { pin: string }) {
+ if (!session) return;
+ const pin = values.pin.trim();
+ if (!/^\d{4}$/.test(pin)) {
+ await showToast({ style: Toast.Style.Failure, title: "Enter the 4-digit PIN shown on your TV" });
+ return;
+ }
+
+ const toast = await showToast({ style: Toast.Style.Animated, title: "Pairing…" });
+ try {
+ const hapCredentials = await finishCompanionPairing(session, pin, "Raycast");
+ const companion = serializeCredentials(hapCredentials);
+ await saveCredentials(device.identifier, { companion, airplay: "" });
+ await saveSelectedDevice(device);
+ toast.style = Toast.Style.Success;
+ toast.title = `Paired with ${device.name}`;
+ toast.message = "You're ready. Try the Apple TV Remote command.";
+ await popToRoot();
+ } catch (e) {
+ toast.style = Toast.Style.Failure;
+ toast.title = "Pairing Failed";
+ toast.message = e instanceof Error ? e.message : String(e);
+ }
+ }
+
+ return (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ );
+}
diff --git a/extensions/apple-tv-remote/src/skip-back.ts b/extensions/apple-tv-remote/src/skip-back.ts
new file mode 100644
index 00000000000..80132b5bc96
--- /dev/null
+++ b/extensions/apple-tv-remote/src/skip-back.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { skipBy } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => skipBy(conn, -10));
+ await showHUD("⏪ -10s");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/skip-forward.ts b/extensions/apple-tv-remote/src/skip-forward.ts
new file mode 100644
index 00000000000..3499631413c
--- /dev/null
+++ b/extensions/apple-tv-remote/src/skip-forward.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { skipBy } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => skipBy(conn, 10));
+ await showHUD("⏩ +10s");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/sleep.ts b/extensions/apple-tv-remote/src/sleep.ts
new file mode 100644
index 00000000000..5225c182d68
--- /dev/null
+++ b/extensions/apple-tv-remote/src/sleep.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { sleepDevice } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sleepDevice(conn));
+ await showHUD("😴 Sleeping");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/tools/launch-app.ts b/extensions/apple-tv-remote/src/tools/launch-app.ts
new file mode 100644
index 00000000000..0ce975581c7
--- /dev/null
+++ b/extensions/apple-tv-remote/src/tools/launch-app.ts
@@ -0,0 +1,38 @@
+import { withConnection } from "../lib/connection";
+import { showErrorToast } from "../lib/errors";
+import { launchApp, listApps } from "../lib/companion-extras";
+import { resolveAppName, saveCachedApps } from "../lib/deep-links";
+
+type Input = {
+ /** App name as the user said it, e.g. "Netflix", "Disney+", "YouTube" */
+ app: string;
+};
+
+/**
+ * Open an app on the Apple TV by name (e.g. Netflix, YouTube, Disney+).
+ * Matches against well-known tvOS apps and the apps actually installed on the device.
+ */
+export default async function launchAppTool(input: Input): Promise {
+ try {
+ // First try resolving against the built-in KNOWN_APPS list (no connection needed).
+ let resolved = resolveAppName(input.app);
+
+ // If unknown, fetch the installed apps from the device and try again.
+ if (!resolved) {
+ const installed = await withConnection(async (conn) => listApps(conn));
+ await saveCachedApps(installed);
+ resolved = resolveAppName(input.app, installed);
+ }
+
+ if (!resolved) {
+ return `Could not find an app named ${input.app} on the Apple TV.`;
+ }
+
+ const { bundleId, name } = resolved;
+ await withConnection(async (conn) => launchApp(conn, bundleId));
+ return `Launched ${name}`;
+ } catch (error) {
+ await showErrorToast(error);
+ throw error;
+ }
+}
diff --git a/extensions/apple-tv-remote/src/tools/play-content.ts b/extensions/apple-tv-remote/src/tools/play-content.ts
new file mode 100644
index 00000000000..f61d91fe066
--- /dev/null
+++ b/extensions/apple-tv-remote/src/tools/play-content.ts
@@ -0,0 +1,24 @@
+import { playContent } from "../lib/play-flow";
+
+type Input = {
+ /** Title of the show or movie, e.g. "Rick and Morty" */
+ title: string;
+ /** Streaming app to use if the user named one, e.g. "Netflix" */
+ app?: string;
+};
+
+/**
+ * Open a specific show or movie on the Apple TV. Resolves the title to a real
+ * streaming deep link (via JustWatch) when the provider supports tvOS deep
+ * linking; for Netflix and unresolved titles it types the title into the
+ * Apple TV's universal Search so the user can pick the result on screen.
+ * Use this whenever the user names a show or movie.
+ */
+export default async function (input: Input): Promise {
+ try {
+ const result = await playContent(input.title, input.app);
+ return result.message;
+ } catch (error) {
+ return `Failed: ${error instanceof Error ? error.message : String(error)}`;
+ }
+}
diff --git a/extensions/apple-tv-remote/src/tools/power.ts b/extensions/apple-tv-remote/src/tools/power.ts
new file mode 100644
index 00000000000..0def56ac9c7
--- /dev/null
+++ b/extensions/apple-tv-remote/src/tools/power.ts
@@ -0,0 +1,35 @@
+import { Tool } from "@raycast/api";
+import { withConnection } from "../lib/connection";
+import { showErrorToast } from "../lib/errors";
+import { sleepDevice, wakeDevice } from "../lib/companion-extras";
+
+type Input = {
+ /** "sleep" to put the Apple TV to sleep, "wake" to wake it up */
+ action: "sleep" | "wake";
+};
+
+/**
+ * Put the Apple TV to sleep or wake it up.
+ */
+export default async function (input: Input): Promise {
+ try {
+ return await withConnection(async (conn) => {
+ if (input.action === "sleep") {
+ await sleepDevice(conn);
+ return "Apple TV is going to sleep.";
+ }
+ await wakeDevice(conn);
+ return "Waking up the Apple TV.";
+ });
+ } catch (e) {
+ await showErrorToast(e);
+ throw e;
+ }
+}
+
+export const confirmation: Tool.Confirmation = async (input) => {
+ if (input.action !== "sleep") {
+ return undefined;
+ }
+ return { message: "Put the Apple TV to sleep?" };
+};
diff --git a/extensions/apple-tv-remote/src/tools/remote-key.ts b/extensions/apple-tv-remote/src/tools/remote-key.ts
new file mode 100644
index 00000000000..7bc2dcb21dd
--- /dev/null
+++ b/extensions/apple-tv-remote/src/tools/remote-key.ts
@@ -0,0 +1,49 @@
+import { AppleTVConnection, RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "../lib/connection";
+import { appSwitcher, controlCenter, longPressSelect, skipBy, startScreensaver } from "../lib/companion-extras";
+
+type Input = {
+ /**
+ * The remote action to perform. One of: up, down, left, right, select, menu, home,
+ * play_pause, volume_up, volume_down, next, previous, context_menu (long-press
+ * select), app_switcher, control_center, screensaver, skip_forward, skip_backward
+ */
+ key: string;
+};
+
+const VALID_KEYS = new Set(Object.values(RemoteKey));
+
+// EXTRAS intentionally shadows the enum's skip keys, the MediaControl path works; the key-press mapping doesn't.
+const EXTRAS: Record Promise> = {
+ context_menu: longPressSelect,
+ long_press_select: longPressSelect,
+ app_switcher: appSwitcher,
+ control_center: controlCenter,
+ screensaver: startScreensaver,
+ skip_forward: (conn) => skipBy(conn, 10),
+ skip_backward: (conn) => skipBy(conn, -10),
+};
+
+/**
+ * Press a single button on the Apple TV remote (navigation, playback, volume),
+ * or perform a remote gesture: context_menu (long-press select), app_switcher,
+ * control_center, screensaver, skip_forward/skip_backward.
+ */
+export default async function tool(input: Input): Promise {
+ const key = input.key.trim().toLowerCase();
+
+ try {
+ const extra = EXTRAS[key];
+ if (extra) {
+ await withConnection(extra);
+ return `Done: ${key.replace(/_/g, " ")}`;
+ }
+ if (!VALID_KEYS.has(key)) {
+ return `Invalid key "${input.key}". Valid: ${[...VALID_KEYS, ...Object.keys(EXTRAS)].join(", ")}.`;
+ }
+ await withConnection((conn) => sendKey(conn, key));
+ return `Pressed ${key}`;
+ } catch (error) {
+ return `Failed to press ${key}: ${error instanceof Error ? error.message : String(error)}`;
+ }
+}
diff --git a/extensions/apple-tv-remote/src/type-text.ts b/extensions/apple-tv-remote/src/type-text.ts
new file mode 100644
index 00000000000..d80521118bd
--- /dev/null
+++ b/extensions/apple-tv-remote/src/type-text.ts
@@ -0,0 +1,13 @@
+import { LaunchProps, showHUD } from "@raycast/api";
+import { setText } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command(props: LaunchProps<{ arguments: { text: string } }>) {
+ try {
+ await withConnection((conn) => setText(conn, props.arguments.text));
+ await showHUD("⌨️ Sent text to Apple TV");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/up.ts b/extensions/apple-tv-remote/src/up.ts
new file mode 100644
index 00000000000..0ec2d17cde6
--- /dev/null
+++ b/extensions/apple-tv-remote/src/up.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.Up));
+ await showHUD("⬆️ Up");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/volume-down.ts b/extensions/apple-tv-remote/src/volume-down.ts
new file mode 100644
index 00000000000..fae81f6849c
--- /dev/null
+++ b/extensions/apple-tv-remote/src/volume-down.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.VolumeDown));
+ await showHUD("🔉 Volume Down");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/volume-up.ts b/extensions/apple-tv-remote/src/volume-up.ts
new file mode 100644
index 00000000000..ade75b83a3e
--- /dev/null
+++ b/extensions/apple-tv-remote/src/volume-up.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { RemoteKey, sendKey } from "@bharper/atv-js";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => sendKey(conn, RemoteKey.VolumeUp));
+ await showHUD("🔊 Volume Up");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/src/wake.ts b/extensions/apple-tv-remote/src/wake.ts
new file mode 100644
index 00000000000..d95b0033267
--- /dev/null
+++ b/extensions/apple-tv-remote/src/wake.ts
@@ -0,0 +1,13 @@
+import { showHUD } from "@raycast/api";
+import { wakeDevice } from "./lib/companion-extras";
+import { withConnection } from "./lib/connection";
+import { showErrorToast } from "./lib/errors";
+
+export default async function Command() {
+ try {
+ await withConnection((conn) => wakeDevice(conn));
+ await showHUD("👋 Waking");
+ } catch (error) {
+ await showErrorToast(error);
+ }
+}
diff --git a/extensions/apple-tv-remote/tsconfig.json b/extensions/apple-tv-remote/tsconfig.json
new file mode 100644
index 00000000000..a409fef4629
--- /dev/null
+++ b/extensions/apple-tv-remote/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "include": ["src/**/*", "raycast-env.d.ts"],
+ "compilerOptions": {
+ "lib": ["ES2023"],
+ "module": "commonjs",
+ "target": "ES2022",
+ "strict": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "jsx": "react-jsx",
+ "resolveJsonModule": true
+ }
+}