diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cec7ad28..de18337f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,12 @@ -# Contributing to torlink +# Contributing to Klink -torlink stays small on purpose. The best way in is to read the code you're about to touch, match how it already works, and keep your change tight. Three recent pull requests set the bar, and this guide points back at them throughout: - -- [#4](https://github.com/baairon/torlink/pull/4) gave the arrow keys spatial pane navigation without breaking a single existing shortcut. -- [#5](https://github.com/baairon/torlink/pull/5) turned a cryptic crash on old Node into a one-line "upgrade me" message. -- [#6](https://github.com/baairon/torlink/pull/6) added copy-magnet, cross-platform, with tests. - -None of them were big. All of them fit the grain. That's the whole idea. +Klink is an enhanced fork of torlink. The best way in is to read the code you're about to touch, match how it already works, and keep your change tight. ## Set up ```sh -git clone https://github.com/baairon/torlink -cd torlink +git clone https://github.com/kaiserc/klink +cd klink npm install npm run dev ``` @@ -71,4 +65,4 @@ torlink is pastel-violet and quiet. There is exactly one gradient, the wordmark - Say why, not just what. The diff already shows the what. - One concern per pull request. Two unrelated ideas are two PRs. -Thanks for helping keep torlink sharp. +Thanks for helping keep Klink sharp. diff --git a/README.md b/README.md index 1ce2fc30..32a1ccb8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Klink expands on upstream torlink with key power-user features: - 🎬 **Direct Media Streaming & Themed Web UI**: Stream video files directly over HTTP while downloading (`klink serve` / `klink files`), with an integrated web interface featuring a dark/light theme switcher. - βœ… **Completed Tab & Smart File Organisation**: Cleanly separate active downloads, seeding items, and finished downloads with directory routing. - ⚠️ **Action Confirmation Dialogs**: Safety confirmation prompts before destructive actions like cancelling downloads or clearing history to prevent accidental data loss. +- πŸ“₯ **Drag-and-Drop & Clipboard `.torrent` Support**: Drop a `.torrent` file directly onto the terminal or paste its path/URI into the search bar to enqueue it instantly. +- πŸ“‘ **Private Tracker Announce Preservation**: Intact tracker list and passkey preservation when loading `.torrent` files. +- ✨ **High-Contrast UI Row Highlights**: Full-row active bolding and dimming across all columns in Results, Downloads, and Seeding views. - πŸ”§ **WebTorrent stability patch**: Guards against a null-pointer crash in `_request` introduced in webtorrent 3.x, keeping the daemon stable under heavy peer churn. --- @@ -40,7 +43,7 @@ Klink expands on upstream torlink with key power-user features: npx klink ``` -That's the only thing you'll type. Klink opens straight to a search bar: search for what you want, paste in a magnet link or a bare infohash, or just press Enter on an empty box to browse the curated library. From there it's all keypresses, nothing to memorise, and `?` brings up the full list anytime. +That's the only thing you'll type. Klink opens straight to a search bar: search for what you want, paste in a magnet link or a bare infohash, drop a `.torrent` file onto the window, or just press Enter on an empty box to browse the curated library. From there it's all keypresses, nothing to memorise, and `?` brings up the full list anytime. ## Finding something @@ -129,6 +132,7 @@ Before opening a PR, skim [CONTRIBUTING.md](CONTRIBUTING.md); it lays out the ba | Media Streaming | ❌ | βœ… | | Completion Tab | ❌ | βœ… | | Confirmation Dialogs | ❌ | βœ… | +| Expanded Categories (E-Books & Audiobooks) | ❌ | βœ… | | WebTorrent null-ref patch | ❌ | βœ… | ## Privacy diff --git a/package-lock.json b/package-lock.json index fb10ccd2..9e2d33ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "klink", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "klink", - "version": "1.6.0", + "version": "1.7.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -22,14 +22,14 @@ "klink": "dist/cli.cjs" }, "devDependencies": { - "@types/node": "^26.1.1", + "@types/node": "^26.2.0", "@types/react": "^19.2.17", "@types/serve-handler": "^6.1.4", "ink-testing-library": "^4.0.0", "tsup": "^8.5.1", - "tsx": "^4.23.1", + "tsx": "^4.23.12", "typescript": "^7.0.2", - "vitest": "^4.1.10" + "vitest": "^4.1.11" }, "engines": { "node": ">=26" @@ -48,44 +48,10 @@ "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", - "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "2.0.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", - "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", - "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -100,9 +66,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -117,9 +83,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -134,9 +100,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -151,9 +117,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -168,9 +134,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -185,9 +151,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -202,9 +168,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -219,9 +185,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -236,9 +202,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -253,9 +219,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -270,9 +236,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -287,9 +253,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -304,9 +270,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -321,9 +287,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -338,9 +304,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -355,9 +321,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -372,9 +338,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -389,9 +355,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -406,9 +372,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -423,9 +389,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -440,9 +406,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -457,9 +423,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -474,9 +440,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -491,9 +457,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -508,9 +474,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -583,42 +549,37 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", - "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", "cpu": [ "arm64" ], @@ -633,9 +594,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", - "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", "cpu": [ "arm64" ], @@ -650,9 +611,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", - "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", "cpu": [ "x64" ], @@ -667,9 +628,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", - "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", "cpu": [ "x64" ], @@ -684,9 +645,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", - "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", "cpu": [ "arm" ], @@ -701,9 +662,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", - "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", "cpu": [ "arm64" ], @@ -721,9 +682,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", - "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", "cpu": [ "arm64" ], @@ -741,9 +702,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", - "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", "cpu": [ "ppc64" ], @@ -761,9 +722,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", - "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", "cpu": [ "s390x" ], @@ -781,9 +742,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", - "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", "cpu": [ "x64" ], @@ -801,9 +762,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", - "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", "cpu": [ "x64" ], @@ -821,9 +782,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", - "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", "cpu": [ "arm64" ], @@ -837,26 +798,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", - "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "2.0.0-alpha.3", - "@emnapi/runtime": "2.0.0-alpha.3", - "@napi-rs/wasm-runtime": "^1.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", - "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", "cpu": [ "arm64" ], @@ -871,9 +816,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", - "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", "cpu": [ "x64" ], @@ -1367,17 +1312,6 @@ "node": ">=0.2.6" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1404,9 +1338,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -1774,16 +1708,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1792,13 +1726,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1819,9 +1753,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1832,13 +1766,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -1846,14 +1780,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1862,9 +1796,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -1872,13 +1806,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1946,9 +1880,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "license": "MIT", "engines": { "node": ">=12" @@ -2052,9 +1986,9 @@ } }, "node_modules/bare-fs": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", - "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.4", @@ -2064,7 +1998,7 @@ "fast-fifo": "^1.3.2" }, "engines": { - "bare": ">=1.16.0" + "bare": ">=1.28.0" }, "peerDependencies": { "bare-buffer": "*" @@ -2145,9 +2079,9 @@ } }, "node_modules/bare-url": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz", - "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", "license": "Apache-2.0", "dependencies": { "bare-path": "^3.0.0" @@ -2944,27 +2878,28 @@ "license": "MIT" }, "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, "node_modules/es-toolkit": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", - "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", "license": "MIT", "workspaces": [ "docs", "benchmarks", - "tests/types" + "tests/types", + "tests/browser-compat" ] }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2975,32 +2910,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-html": { @@ -3480,9 +3415,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "license": "MIT", "engines": { "node": ">= 12" @@ -3502,9 +3437,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", "license": "MIT", "engines": { "node": ">= 10" @@ -4192,9 +4127,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -4502,9 +4437,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -4522,7 +4457,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4809,13 +4744,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", - "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.142.0", + "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4825,21 +4760,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.1", - "@rolldown/binding-darwin-arm64": "1.2.1", - "@rolldown/binding-darwin-x64": "1.2.1", - "@rolldown/binding-freebsd-x64": "1.2.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", - "@rolldown/binding-linux-arm64-gnu": "1.2.1", - "@rolldown/binding-linux-arm64-musl": "1.2.1", - "@rolldown/binding-linux-ppc64-gnu": "1.2.1", - "@rolldown/binding-linux-s390x-gnu": "1.2.1", - "@rolldown/binding-linux-x64-gnu": "1.2.1", - "@rolldown/binding-linux-x64-musl": "1.2.1", - "@rolldown/binding-openharmony-arm64": "1.2.1", - "@rolldown/binding-wasm32-wasi": "1.2.1", - "@rolldown/binding-win32-arm64-msvc": "1.2.1", - "@rolldown/binding-win32-x64-msvc": "1.2.1" + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, "node_modules/rollup": { @@ -5554,14 +5489,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", @@ -5616,9 +5543,9 @@ } }, "node_modules/tsx": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", - "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5831,16 +5758,16 @@ } }, "node_modules/vite": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", - "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.23", - "rolldown": "~1.2.0", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "bin": { @@ -5857,7 +5784,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -5909,19 +5836,19 @@ } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -5949,12 +5876,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -6164,14 +6091,13 @@ } }, "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.1.tgz", + "integrity": "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -6187,9 +6113,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 31391f47..fdd4a597 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "klink", - "version": "1.6.0", + "version": "1.7.0", "description": "Klink - A sleek, zero-setup torrent finder and downloader TUI with peer inspection, media streaming, and turtle mode.", "type": "module", "bin": { @@ -67,26 +67,27 @@ "webtorrent": "^2.8.5" }, "devDependencies": { - "@types/node": "^26.1.1", + "@types/node": "^26.2.0", "@types/react": "^19.2.17", "@types/serve-handler": "^6.1.4", "ink-testing-library": "^4.0.0", "tsup": "^8.5.1", - "tsx": "^4.23.1", + "tsx": "^4.23.12", "typescript": "^7.0.2", - "vitest": "^4.1.10" + "vitest": "^4.1.11" }, "allowScripts": { "bufferutil@4.1.0": true, "esbuild@0.27.7": true, "esbuild@0.28.1": true, + "esbuild@0.28.2": true, "ip-set@3.0.0": true, "node-datachannel@0.32.3": true, "utf-8-validate@6.0.6": true, "utp-native@2.5.3": true }, "overrides": { - "esbuild": "^0.28.1", + "esbuild": "^0.28.2", "ip": "^2.0.1", "uint8-util": "2.2.6" } diff --git a/src/config/folder.ts b/src/config/folder.ts index 3cadd33a..459cab07 100644 --- a/src/config/folder.ts +++ b/src/config/folder.ts @@ -1,13 +1,22 @@ import os from "node:os"; import path from "node:path"; +// node:path doesn't export its interface by name, so borrow it off a member. +type PlatformPath = typeof path.win32; + // We read the raw input field, so expand a leading ~ ourselves (~\ too, for // paths pasted from Windows). ~bob isn't us, so leave it alone. -export function expandHome(input: string, home: string = os.homedir()): string { +// `p` lets a caller reasoning about one platform's paths (and its tests) pass +// path.win32 / path.posix instead of the host's flavour; it defaults to the host. +export function expandHome( + input: string, + home: string = os.homedir(), + p: PlatformPath = path, +): string { const trimmed = input.trim(); if (trimmed === "~") return home; if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) { - return path.join(home, trimmed.slice(2)); + return p.join(home, trimmed.slice(2)); } return trimmed; } diff --git a/src/parse-torrent.d.ts b/src/parse-torrent.d.ts index 57623ce6..c7a11259 100644 --- a/src/parse-torrent.d.ts +++ b/src/parse-torrent.d.ts @@ -2,6 +2,7 @@ declare module "parse-torrent" { interface ParsedTorrent { infoHash: string; name?: string; + announce?: string[]; } export default function parseTorrent( torrentId: Uint8Array | string, diff --git a/src/sources/magnet.test.ts b/src/sources/magnet.test.ts index 9ef91ea4..47af92c9 100644 --- a/src/sources/magnet.test.ts +++ b/src/sources/magnet.test.ts @@ -49,6 +49,20 @@ describe("buildMagnet", () => { // At least one non-UDP tracker, so UDP-blocked networks can still announce. expect(out).toContain(encodeURIComponent("http://tracker.opentrackr.org:1337/announce")); }); + it("puts extra trackers ahead of the public defaults", () => { + const own = "http://private.example.org/announce?pk=xyz"; + const out = buildMagnet("abc123", "Thing", [own]); + const mine = out.indexOf(`&tr=${encodeURIComponent(own)}`); + expect(mine).toBeGreaterThan(-1); + expect(mine).toBeLessThan( + out.indexOf(encodeURIComponent("udp://tracker.opentrackr.org:1337/announce")), + ); + }); + it("keeps one copy of a tracker that is also a default, and drops blanks", () => { + const dupe = "udp://tracker.opentrackr.org:1337/announce"; + const out = buildMagnet("abc123", "Thing", [dupe, " ", dupe]); + expect(out.split(`&tr=${encodeURIComponent(dupe)}`).length - 1).toBe(1); + }); }); describe("isInfoHash", () => { diff --git a/src/sources/magnet.ts b/src/sources/magnet.ts index 84f7061a..99079433 100644 --- a/src/sources/magnet.ts +++ b/src/sources/magnet.ts @@ -14,9 +14,18 @@ const TRACKERS = [ "https://tracker.tamersunion.org:443/announce", ]; -export function buildMagnet(infoHash: string, name: string): string { +// extraTrackers come first and win on duplicates: a torrent that carries its own +// announce list means that list, and the public defaults are only a fallback. +export function buildMagnet(infoHash: string, name: string, extraTrackers: string[] = []): string { const dn = encodeURIComponent(name); - const tr = TRACKERS.map((t) => `&tr=${encodeURIComponent(t)}`).join(""); + const seen = new Set(); + const trackers = [...extraTrackers, ...TRACKERS].filter((t) => { + const url = t.trim(); + if (!url || seen.has(url)) return false; + seen.add(url); + return true; + }); + const tr = trackers.map((t) => `&tr=${encodeURIComponent(t)}`).join(""); return `magnet:?xt=urn:btih:${infoHash}&dn=${dn}${tr}`; } diff --git a/src/sources/piratebay.test.ts b/src/sources/piratebay.test.ts new file mode 100644 index 00000000..7b0585c2 --- /dev/null +++ b/src/sources/piratebay.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { tpbMovies } from "./piratebay"; +import { fetchResilient } from "../util/net"; + +vi.mock("../util/net", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, fetchResilient: vi.fn() }; +}); + +const mockFetch = vi.mocked(fetchResilient); + +const page = (items: unknown[]): Response => + ({ ok: true, status: 200, json: async () => items }) as unknown as Response; + +// apibay answers an empty search with this single placeholder instead of []. +const SENTINEL = { + id: "0", + name: "No results returned", + info_hash: "0000000000000000000000000000000000000000", + category: "0", +}; + +const movieRow = { + id: "1", + name: "Dune Part Two 2024 1080p", + info_hash: "a".repeat(40), + seeders: "12", + leechers: "3", + size: "4000", + num_files: "2", + added: "1700000000", + category: "207", +}; + +const askedUrl = (call: number): string => String(mockFetch.mock.calls[call]![0]); + +beforeEach(() => { + mockFetch.mockReset(); +}); + +// apibay caches search results per exact URL, and a query can be stuck with +// the sentinel on one URL form while the alternate form answers fine (live: +// q=metallica is poisoned bare but healthy with &cat=0; q=dune the other way +// around). One retry on the other form re-rolls that cache key instead of +// showing the user an empty column. +describe("apibay sentinel retry", () => { + it("retries with cat=0 when a search comes back as the no-results sentinel", async () => { + mockFetch.mockResolvedValueOnce(page([SENTINEL])); + mockFetch.mockResolvedValueOnce(page([movieRow])); + const results = await tpbMovies.search("metallica"); + expect(results.map((r) => r.name)).toEqual([movieRow.name]); + expect(askedUrl(0)).toBe("https://apibay.org/q.php?q=metallica"); + expect(askedUrl(1)).toBe("https://apibay.org/q.php?q=metallica&cat=0"); + }); + + it("asks once when the search returns real rows", async () => { + mockFetch.mockResolvedValueOnce(page([movieRow])); + await tpbMovies.search("dune"); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("returns empty when both forms answer with the sentinel", async () => { + mockFetch.mockResolvedValueOnce(page([SENTINEL])); + mockFetch.mockResolvedValueOnce(page([SENTINEL])); + expect(await tpbMovies.search("qqqqzzzz")).toEqual([]); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it("never retries a browse, which hits the precompiled feed", async () => { + mockFetch.mockResolvedValueOnce(page([movieRow])); + await tpbMovies.search(""); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(askedUrl(0)).toContain("/precompiled/"); + }); +}); diff --git a/src/sources/piratebay.ts b/src/sources/piratebay.ts index 08b1fe8f..074fdee0 100644 --- a/src/sources/piratebay.ts +++ b/src/sources/piratebay.ts @@ -57,6 +57,21 @@ async function fetchItems(url: string, opts: SearchOptions): Promise { + const items = await fetchItems(`${API}/q.php?q=${encodeURIComponent(q)}`, opts); + if (!isNoResultsSentinel(items)) return items; + return fetchItems(`${API}/q.php?q=${encodeURIComponent(q)}&cat=0`, opts); +} + async function search( query: string, cats: Set, @@ -65,10 +80,7 @@ async function search( opts: SearchOptions, ): Promise { const q = query.trim(); - const items = await fetchItems( - q ? `${API}/q.php?q=${encodeURIComponent(q)}` : browseUrl, - opts, - ); + const items = q ? await searchItems(q, opts) : await fetchItems(browseUrl, opts); const out: TorrentResult[] = []; for (const it of items) { if (q && !cats.has(Number(it.category))) continue; diff --git a/src/sources/torrentFile.test.ts b/src/sources/torrentFile.test.ts new file mode 100644 index 00000000..71f33795 --- /dev/null +++ b/src/sources/torrentFile.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { magnetFromTorrentFile } from "./torrentFile"; + +// Hand-rolled bencode, so the fixture is a real .torrent parse-torrent accepts +// rather than a checked-in binary blob. +function bstr(s: string): Buffer { + const b = Buffer.from(s, "utf8"); + return Buffer.concat([Buffer.from(`${b.length}:`), b]); +} + +function makeTorrent(announce: string[]): Buffer { + const info = Buffer.concat([ + Buffer.from("d"), + bstr("length"), + Buffer.from("i1024e"), + bstr("name"), + bstr("test.bin"), + bstr("piece length"), + Buffer.from("i16384e"), + bstr("pieces"), + Buffer.from("20:"), + Buffer.alloc(20, 7), + Buffer.from("e"), + ]); + const head: Buffer[] = [Buffer.from("d")]; + if (announce[0]) head.push(bstr("announce"), bstr(announce[0])); + if (announce.length) { + head.push(bstr("announce-list"), Buffer.from("l")); + for (const url of announce) head.push(Buffer.from("l"), bstr(url), Buffer.from("e")); + head.push(Buffer.from("e")); + } + return Buffer.concat([...head, bstr("info"), info, Buffer.from("e")]); +} + +let dir: string; + +beforeAll(async () => { + dir = await fs.mkdtemp(path.join(os.tmpdir(), "torlink-torrentfile-")); +}); + +afterAll(async () => { + await fs.rm(dir, { recursive: true, force: true }); +}); + +async function write(name: string, data: Buffer): Promise { + const file = path.join(dir, name); + await fs.writeFile(file, data); + return file; +} + +describe("magnetFromTorrentFile", () => { + it("reads the info hash and name out of a .torrent", async () => { + const file = await write("plain.torrent", makeTorrent([])); + const parsed = await magnetFromTorrentFile(file); + expect(parsed?.name).toBe("test.bin"); + expect(parsed?.infoHash).toMatch(/^[a-f0-9]{40}$/); + }); + + it("puts the torrent's own trackers in the magnet, ahead of the public ones", async () => { + const own = "http://private.example.org/announce?pk=xyz"; + const file = await write("private.torrent", makeTorrent([own])); + const parsed = await magnetFromTorrentFile(file); + const mine = parsed!.magnet.indexOf(`&tr=${encodeURIComponent(own)}`); + expect(mine).toBeGreaterThan(-1); + expect(mine).toBeLessThan( + parsed!.magnet.indexOf(encodeURIComponent("udp://tracker.opentrackr.org:1337/announce")), + ); + }); + + it("keeps one copy of a tracker the defaults already carry", async () => { + const dupe = "udp://tracker.opentrackr.org:1337/announce"; + const file = await write("dupe.torrent", makeTorrent([dupe])); + const parsed = await magnetFromTorrentFile(file); + const hits = parsed!.magnet.split(`&tr=${encodeURIComponent(dupe)}`).length - 1; + expect(hits).toBe(1); + }); + + it("returns null for a missing file, a directory, an empty file, and junk", async () => { + expect(await magnetFromTorrentFile(path.join(dir, "nope.torrent"))).toBe(null); + expect(await magnetFromTorrentFile(dir)).toBe(null); + expect(await magnetFromTorrentFile(await write("empty.torrent", Buffer.alloc(0)))).toBe(null); + expect( + await magnetFromTorrentFile(await write("junk.torrent", Buffer.from("not a torrent"))), + ).toBe(null); + }); + + it("refuses a file too large to be metadata rather than reading it in", async () => { + const big = await write("big.torrent", Buffer.alloc(17 * 1024 * 1024)); + expect(await magnetFromTorrentFile(big)).toBe(null); + }); +}); diff --git a/src/sources/torrentFile.ts b/src/sources/torrentFile.ts index 42397903..321f2cb8 100644 --- a/src/sources/torrentFile.ts +++ b/src/sources/torrentFile.ts @@ -2,14 +2,28 @@ import { promises as fs } from "node:fs"; import parseTorrent from "parse-torrent"; import { buildMagnet, type ParsedMagnet } from "./magnet"; +// A .torrent is metadata, not payload: even a torrent with tens of thousands of +// pieces stays in the low megabytes. The cap is what keeps a mis-named disk +// image dropped in the watch folder from being pulled into memory whole. +const MAX_TORRENT_BYTES = 16 * 1024 * 1024; + export async function magnetFromTorrentFile(path: string): Promise { try { + const stat = await fs.stat(path); + if (!stat.isFile() || stat.size === 0 || stat.size > MAX_TORRENT_BYTES) return null; const buf = await fs.readFile(path); const parsed = await parseTorrent(new Uint8Array(buf)); const infoHash = parsed?.infoHash?.toLowerCase(); if (!infoHash) return null; const name = parsed.name || infoHash; - return { infoHash, name, magnet: buildMagnet(infoHash, name) }; + // Carry the file's own announce list into the magnet. Without it a torrent + // that isn't on the public DHT β€” a private tracker, a small private swarm β€” + // sits at zero peers forever, and on a private tracker the passkey that + // makes an announce work at all lives in that URL. + const announce = Array.isArray(parsed.announce) + ? parsed.announce.filter((url): url is string => typeof url === "string") + : []; + return { infoHash, name, magnet: buildMagnet(infoHash, name, announce) }; } catch { return null; } diff --git a/src/sources/torrentPath.test.ts b/src/sources/torrentPath.test.ts new file mode 100644 index 00000000..b556b851 --- /dev/null +++ b/src/sources/torrentPath.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from "vitest"; +import { + unquote, + fromFileUrl, + resolveTorrentPath, + type TorrentPathOptions, +} from "./torrentPath"; + +const WIN: TorrentPathOptions = { windows: true, home: "C:\\Users\\u" }; +const NIX: TorrentPathOptions = { windows: false, home: "/home/u" }; + +describe("unquote", () => { + it("strips matched double and single quotes", () => { + expect(unquote('"a"')).toBe("a"); + expect(unquote("'a'")).toBe("a"); + }); + it("strips trailing space dropped paths arrive with", () => { + expect(unquote('"C:\\a.torrent" ')).toBe("C:\\a.torrent"); + expect(unquote(" /a.torrent ")).toBe("/a.torrent"); + }); + it("leaves unmatched quotes alone", () => { + expect(unquote('"a')).toBe('"a'); + expect(unquote("a'")).toBe("a'"); + }); + it("handles empty and single-char input", () => { + expect(unquote("")).toBe(""); + expect(unquote('"')).toBe('"'); + }); +}); + +describe("fromFileUrl", () => { + it("converts a POSIX file URI", () => { + expect(fromFileUrl("file:///home/u/a.torrent", false)).toBe("/home/u/a.torrent"); + expect(fromFileUrl("file://localhost/home/u/a.torrent", false)).toBe("/home/u/a.torrent"); + }); + it("converts a Windows file URI and strips the slash before the drive", () => { + expect(fromFileUrl("file:///C:/Users/u/a.torrent", true)).toBe("C:/Users/u/a.torrent"); + }); + it("decodes percent-encoded spaces and UTF-8", () => { + expect(fromFileUrl("file:///home/u/My%20Show.torrent", false)).toBe( + "/home/u/My Show.torrent", + ); + }); + it("returns null for non-file URIs and bad escapes", () => { + expect(fromFileUrl("https://example.com/a.torrent", false)).toBe(null); + expect(fromFileUrl("file:///bad%xx.torrent", false)).toBe(null); + expect(fromFileUrl("/not/a/url", false)).toBe(null); + }); +}); + +describe("resolveTorrentPath", () => { + it("strips the double quotes Windows Terminal wraps dropped paths in", () => { + expect(resolveTorrentPath('"C:\\Users\\u\\Downloads\\My Show.torrent" ', WIN)).toBe( + "C:\\Users\\u\\Downloads\\My Show.torrent", + ); + }); + it("keeps Windows backslashes rather than reading them as escapes", () => { + expect(resolveTorrentPath("C:\\Users\\u\\a.torrent", WIN)).toBe("C:\\Users\\u\\a.torrent"); + }); + it("unescapes the backslashes macOS and Linux terminals add for spaces", () => { + expect(resolveTorrentPath("/home/u/My\\ Show\\ \\(2024\\).torrent", NIX)).toBe( + "/home/u/My Show (2024).torrent", + ); + }); + it("resolves the file:// URI GNOME Terminal pastes", () => { + expect(resolveTorrentPath("file:///home/u/My%20Show.torrent", NIX)).toBe( + "/home/u/My Show.torrent", + ); + }); + it("expands a typed ~ path", () => { + expect(resolveTorrentPath("~/Downloads/a.torrent", NIX)).toBe("/home/u/Downloads/a.torrent"); + }); + it("accepts an uppercase extension", () => { + expect(resolveTorrentPath("/home/u/A.TORRENT", NIX)).toBe("/home/u/A.TORRENT"); + }); + it("returns null for a search query, a magnet, and empty input", () => { + expect(resolveTorrentPath("the matrix 1999", NIX)).toBe(null); + expect(resolveTorrentPath("magnet:?xt=urn:btih:" + "a".repeat(40), NIX)).toBe(null); + expect(resolveTorrentPath(" ", NIX)).toBe(null); + }); + it("returns null for a file that isn't a .torrent", () => { + expect(resolveTorrentPath('"C:\\Users\\u\\holiday.mp4"', WIN)).toBe(null); + }); +}); diff --git a/src/sources/torrentPath.ts b/src/sources/torrentPath.ts new file mode 100644 index 00000000..bdf37c2d --- /dev/null +++ b/src/sources/torrentPath.ts @@ -0,0 +1,66 @@ +// A file dragged onto a terminal window never arrives as a clean path: every +// emulator escapes it its own way. Windows Terminal and PowerShell wrap it in +// double quotes (and leave a trailing space), macOS Terminal and iTerm2 escape +// spaces and parens with backslashes, GNOME Terminal pastes a file:// URI. +// Normalize all of those back to a plain path before we touch the disk. + +import os from "node:os"; +import path from "node:path"; +import { expandHome } from "../config/folder"; + +export interface TorrentPathOptions { + home?: string; + // Platform of the *path*, not of the process: a Windows path keeps its + // backslashes, a POSIX one has them stripped as escapes. Defaults to the + // host, and is passed explicitly by the tests so all three shapes are + // checked on every OS rather than only on the one that produces them. + windows?: boolean; +} + +// Strips one matching pair of surrounding quotes. Callers get a trimmed string +// either way, so a dropped path's trailing space is gone before anything else. +export function unquote(input: string): string { + const s = input.trim(); + const first = s[0]; + if (s.length >= 2 && (first === '"' || first === "'") && s[s.length - 1] === first) { + return s.slice(1, -1).trim(); + } + return s; +} + +// file:///home/u/a.torrent -> /home/u/a.torrent, file:///C:/u/a.torrent -> C:/u/a.torrent. +// Decoded by hand rather than via fileURLToPath so the result depends on the +// path's platform, not the running one. Returns null for anything not a file URI. +export function fromFileUrl(input: string, windows: boolean): string | null { + const m = /^file:\/\/(?:localhost)?(\/.*)$/i.exec(input); + if (!m) return null; + let p: string; + try { + p = decodeURIComponent(m[1]!); + } catch { + return null; // a stray % that isn't an escape + } + // A Windows URI carries a leading slash before the drive letter: /C:/x -> C:/x. + if (windows && /^\/[a-z]:/i.test(p)) p = p.slice(1); + return p; +} + +// Raw input field text -> a path to read, or null if this isn't a .torrent path +// at all (an ordinary search query, a magnet link). Existence isn't checked +// here; the caller reads the file and reports its own failure. +export function resolveTorrentPath(raw: string, options: TorrentPathOptions = {}): string | null { + const windows = options.windows ?? process.platform === "win32"; + const home = options.home ?? os.homedir(); + const unquoted = unquote(raw); + if (!unquoted) return null; + + const p = windows ? path.win32 : path.posix; + const url = fromFileUrl(unquoted, windows); + // Backslash is a path separator on Windows, so only POSIX unescapes: there + // `My\ Files` means one folder named "My Files". + const bare = url ?? (windows ? unquoted : unquoted.replace(/\\(.)/g, "$1")); + if (!/\.torrent$/i.test(bare)) return null; + // A URI is already absolute; only typed input can carry a ~. + const expanded = url ?? expandHome(bare, home, p); + return p.normalize(expanded); +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 5cc110f0..7a3ca67f 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -17,6 +17,7 @@ import { applyNetworkBinding } from "../download/network"; import { logCrash } from "../util/crashlog"; import { parseInput } from "../sources/magnet"; import { magnetFromTorrentFile } from "../sources/torrentFile"; +import { resolveTorrentPath } from "../sources/torrentPath"; import { readClipboard, writeClipboard } from "../util/clipboard"; import { openFolder } from "../util/openFolder"; import { startWebServer, stopWebServer } from "../server"; @@ -190,7 +191,7 @@ export function App({ const launch = initialMagnet ? parseInput(initialMagnet) : initialTorrent - ? await magnetFromTorrentFile(initialTorrent) + ? await magnetFromTorrentFile(resolveTorrentPath(initialTorrent) ?? initialTorrent) : null; if (launch) { await fs.mkdir(cfg.downloadDir, { recursive: true }).catch(() => {}); @@ -488,6 +489,25 @@ export function App({ [queue, config], ); + // A .torrent dragged onto the terminal lands in the search field as a path. + // Read it, hand the queue the magnet built from its metadata, and say so when + // it can't be read rather than quietly searching for the path text. + const startFromTorrentFile = useCallback( + (file: string) => { + setNotice(`Reading torrent file: ${truncate(file, 48)}`); + void (async () => { + const parsed = await magnetFromTorrentFile(file); + if (!parsed) { + setNotice(`Couldn't read a torrent from ${truncate(file, 48)}.`); + return; + } + startDownload({ id: parsed.infoHash, name: parsed.name, magnet: parsed.magnet }); + })(); + setView("browser"); + }, + [startDownload], + ); + const submitQuery = useCallback( (raw: string) => { const q = raw.trim(); @@ -502,13 +522,18 @@ export function App({ setView("browser"); return; } + const file = resolveTorrentPath(q); + if (file) { + startFromTorrentFile(file); + return; + } } setQuery(q); setView("browser"); if (section === "downloads") setSection("all"); setRegion("content"); }, - [section, startDownload], + [section, startDownload, startFromTorrentFile], ); const pasteFromClipboard = useCallback(async () => { @@ -524,8 +549,15 @@ export function App({ setView("browser"); return; } - setNotice("No magnet link on the clipboard."); - }, [startDownload]); + // Copying a file in a file manager puts its path on the clipboard, so paste + // takes one too β€” same handling as a drag onto the search field. + const file = resolveTorrentPath(text); + if (file) { + startFromTorrentFile(file); + return; + } + setNotice("No magnet link or .torrent file on the clipboard."); + }, [startDownload, startFromTorrentFile]); useEffect(() => { if (!notice) return; diff --git a/src/ui/components/Downloads.tsx b/src/ui/components/Downloads.tsx index 0fab3b0d..646145e1 100644 --- a/src/ui/components/Downloads.tsx +++ b/src/ui/components/Downloads.tsx @@ -223,11 +223,18 @@ export function Downloads() { - {it.totalBytes > 0 ? formatBytes(it.totalBytes) : "-"} + {it.totalBytes > 0 ? formatBytes(it.totalBytes) : "-"} + - - {it.source ? ss.tag : "mag"} + {it.source ? ss.tag : "mag"} @@ -280,14 +287,25 @@ export function Downloads() { - {h.sizeBytes > 0 ? formatBytes(h.sizeBytes) : "-"} + {h.sizeBytes > 0 ? formatBytes(h.sizeBytes) : "-"} + - {when || "-"} + {when || "-"} + - - {h.source ? ss.tag : "mag"} + {h.source ? ss.tag : "mag"} diff --git a/src/ui/components/Results.tsx b/src/ui/components/Results.tsx index 249262d3..3c60244c 100644 --- a/src/ui/components/Results.tsx +++ b/src/ui/components/Results.tsx @@ -463,20 +463,35 @@ export function Results() { - {r.sizeBytes > 0 ? formatBytes(r.sizeBytes) : "-"} + {r.sizeBytes > 0 ? formatBytes(r.sizeBytes) : "-"} + - 0 ? COLOR.good : undefined} dimColor={r.seeders === 0}> - {r.seeders || r.leechers + 0 ? COLOR.good : undefined} + dimColor={!here} + bold={here} + >{r.seeders || r.leechers ? `${formatCount(r.seeders)}:${formatCount(r.leechers)}` : "-"} - {formatRelative(r.added) || "-"} + {formatRelative(r.added) || "-"} + - + {ss.tag} diff --git a/src/ui/components/Seeding.tsx b/src/ui/components/Seeding.tsx index e4c7d348..c7f509ed 100644 --- a/src/ui/components/Seeding.tsx +++ b/src/ui/components/Seeding.tsx @@ -171,14 +171,26 @@ export function Seeding() { - {h.sizeBytes > 0 ? formatBytes(h.sizeBytes) : "-"} + {h.sizeBytes > 0 ? formatBytes(h.sizeBytes) : "-"} + - {truncate(st.text, STATUS_W)} + {truncate(st.text, STATUS_W)} + - - {h.source ? ss.tag : "mag"} + {h.source ? ss.tag : "mag"} diff --git a/src/ui/views/Splash.tsx b/src/ui/views/Splash.tsx index a91fc9ba..843c0f3d 100644 --- a/src/ui/views/Splash.tsx +++ b/src/ui/views/Splash.tsx @@ -58,7 +58,7 @@ export function Splash({ width={barWidth} value="" editing - placeholder="Search or paste a magnet link…" + placeholder="Search, paste a magnet, or drop a .torrent…" onSubmit={submitQuery} onExitDown={() => submitQuery("")} />