Personal save-for-later and Miniflux e-reader proxy for Xteink X4 (wip)
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Patrick Dewey a4c08c77

+3811
+4
.env.example
··· 1 + MINIFLUX_URL=http://miniflux.local:8080 2 + MINIFLUX_TOKEN=paste-your-miniflux-api-key-here 3 + NIGHTSHADE_PORT=8787 4 + NIGHTSHADE_DB=./nightshade.sqlite3
+6
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + *.sqlite3 4 + *.sqlite3-* 5 + .env 6 + .env.local
+127
README.md
··· 1 + # nightshade 2 + 3 + Personal save-for-later + e-reader proxy. Sits alongside an existing 4 + [Miniflux](https://miniflux.app/) instance and adds: 5 + 6 + - a save-arbitrary-URL flow (fetch → readability → local SQLite) 7 + - a plain-text HTTP API for an e-ink device to paginate through unread items 8 + - a tiny Preact UI to subscribe to feeds, save URLs, and manage both lists 9 + 10 + RSS subscriptions and entries live in Miniflux. Nightshade only stores the 11 + one-off saved URLs. 12 + 13 + ## Stack 14 + 15 + - Node + TypeScript 16 + - Hono + `@hono/node-server` 17 + - `better-sqlite3` for local saves 18 + - `@mozilla/readability` + `linkedom` for article extraction 19 + - Preact + Vite for the frontend 20 + - pnpm 21 + 22 + ## Development 23 + 24 + ```sh 25 + pnpm install 26 + cp .env.example .env # fill in MINIFLUX_URL + MINIFLUX_TOKEN 27 + pnpm dev # runs server (8787) and Vite (5173) in parallel 28 + ``` 29 + 30 + The Vite dev server proxies `/api` and `/device` to the Node server on 8787. 31 + 32 + Production build: 33 + 34 + ```sh 35 + pnpm build # vite build → dist/public 36 + pnpm start # tsx src/server/index.ts, serves dist/public as SPA 37 + ``` 38 + 39 + ## Environment 40 + 41 + | var | default | purpose | 42 + |---|---|---| 43 + | `MINIFLUX_URL` | — | required, base URL of your Miniflux instance | 44 + | `MINIFLUX_TOKEN` | — | required (or `MINIFLUX_TOKEN_FILE`), Miniflux API key | 45 + | `MINIFLUX_TOKEN_FILE` | — | path to file containing the token (systemd credential) | 46 + | `NIGHTSHADE_PORT` | `8787` | HTTP port | 47 + | `NIGHTSHADE_DB` | `./nightshade.sqlite3` | SQLite file path | 48 + 49 + ## HTTP API 50 + 51 + ### Browser / management (JSON) 52 + 53 + - `GET /api/saves[?all=1]` — list saved items 54 + - `POST /api/saves` — save a URL (body `{url}`) 55 + - `DELETE /api/saves/:id` — remove a saved item 56 + - `POST /api/saves/:id/read` — mark read 57 + - `POST /api/saves/:id/unread` — mark unread 58 + - `GET /api/feeds` — list Miniflux subscriptions 59 + - `POST /api/feeds` — subscribe (body `{url, category_id?}`) 60 + - `DELETE /api/feeds/:id` — unsubscribe 61 + - `POST /api/feeds/refresh` — refresh all feeds 62 + 63 + ### E-reader device (plain text) 64 + 65 + No auth, no TLS — LAN only. 66 + 67 + - `GET /device/list[?all=1&limit=N]` — merged list of unread items 68 + - `GET /device/item/:id[?page=N]` — one item, paginated (60-col wrap, 24 lines/page) 69 + - `POST /device/item/:id/read` — mark read 70 + 71 + IDs: Miniflux entries use the numeric entry id; saved items use `s<id>`. 72 + 73 + List format: 74 + 75 + ``` 76 + COUNT:<n> 77 + === 78 + ID:<id> 79 + S:<rss|save> 80 + R:<0|1> 81 + D:<unix> 82 + T:<title> 83 + === 84 + ... 85 + ``` 86 + 87 + Item format: 88 + 89 + ``` 90 + ID:<id> 91 + S:<rss|save> 92 + T:<title> 93 + U:<url> 94 + D:<unix> 95 + P:<cur>/<total> 96 + --- 97 + <wrapped body for this page> 98 + ``` 99 + 100 + ## NixOS 101 + 102 + ```nix 103 + { 104 + inputs.nightshade.url = "path:/path/to/nightshade"; 105 + 106 + outputs = { nixpkgs, nightshade, ... }: { 107 + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { 108 + modules = [ 109 + nightshade.nixosModules.default 110 + { 111 + services.nightshade = { 112 + enable = true; 113 + minifluxUrl = "http://miniflux.lan:8080"; 114 + minifluxTokenFile = "/run/secrets/miniflux-token"; 115 + openFirewall = true; 116 + }; 117 + } 118 + ]; 119 + }; 120 + }; 121 + } 122 + ``` 123 + 124 + The flake package vendors `node_modules/` and `dist/` from the source tree. 125 + Run `pnpm install && pnpm build` before `nix build`. A proper pure-Nix build 126 + would need `pnpm2nix` or FOD'd installs, which isn't worth it for a personal 127 + tool.
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1776548001, 6 + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", 7 + "owner": "NixOS", 8 + "repo": "nixpkgs", 9 + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "NixOS", 14 + "ref": "nixos-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+144
flake.nix
··· 1 + { 2 + description = "Nightshade — personal save-for-later + e-reader proxy for Miniflux"; 3 + 4 + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 + 6 + outputs = { self, nixpkgs }: 7 + let 8 + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 9 + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: 10 + f { 11 + inherit system; 12 + pkgs = import nixpkgs { inherit system; }; 13 + }); 14 + 15 + mkPackage = pkgs: pkgs.stdenv.mkDerivation { 16 + pname = "nightshade"; 17 + version = "0.1.0"; 18 + src = ./.; 19 + 20 + nativeBuildInputs = [ pkgs.makeWrapper ]; 21 + buildInputs = [ pkgs.nodejs_22 ]; 22 + 23 + # Node deps (including better-sqlite3's compiled native addon) and the 24 + # Vite build must be produced before `nix build`: 25 + # pnpm install && pnpm build 26 + # This derivation vendors node_modules/ and dist/ from the source tree. 27 + dontBuild = true; 28 + 29 + installPhase = '' 30 + mkdir -p $out/share/nightshade 31 + cp -r \ 32 + package.json \ 33 + tsconfig.json \ 34 + src \ 35 + dist \ 36 + node_modules \ 37 + $out/share/nightshade/ 38 + 39 + mkdir -p $out/bin 40 + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/nightshade \ 41 + --add-flags "--import=tsx" \ 42 + --add-flags "$out/share/nightshade/src/server/index.ts" \ 43 + --prefix NODE_PATH : "$out/share/nightshade/node_modules" 44 + ''; 45 + }; 46 + in { 47 + packages = forAllSystems ({ pkgs, ... }: { 48 + default = mkPackage pkgs; 49 + nightshade = mkPackage pkgs; 50 + }); 51 + 52 + devShells = forAllSystems ({ pkgs, ... }: { 53 + default = pkgs.mkShell { 54 + buildInputs = with pkgs; [ nodejs_22 pnpm ]; 55 + }; 56 + }); 57 + 58 + nixosModules.default = { config, lib, pkgs, ... }: 59 + let 60 + cfg = config.services.nightshade; 61 + pkg = self.packages.${pkgs.system}.nightshade; 62 + in { 63 + options.services.nightshade = { 64 + enable = lib.mkEnableOption "Nightshade e-reader proxy"; 65 + port = lib.mkOption { 66 + type = lib.types.port; 67 + default = 8787; 68 + }; 69 + dataDir = lib.mkOption { 70 + type = lib.types.path; 71 + default = "/var/lib/nightshade"; 72 + }; 73 + user = lib.mkOption { 74 + type = lib.types.str; 75 + default = "nightshade"; 76 + }; 77 + openFirewall = lib.mkOption { 78 + type = lib.types.bool; 79 + default = false; 80 + }; 81 + minifluxUrl = lib.mkOption { 82 + type = lib.types.str; 83 + example = "http://192.168.4.71:18800"; 84 + description = "Base URL of the Miniflux instance."; 85 + }; 86 + minifluxTokenFile = lib.mkOption { 87 + type = lib.types.path; 88 + description = '' 89 + Path to a file containing the Miniflux API token. 90 + Readable by the nightshade user. Not placed in the Nix store. 91 + ''; 92 + }; 93 + }; 94 + 95 + config = lib.mkIf cfg.enable { 96 + users.users.${cfg.user} = { 97 + isSystemUser = true; 98 + group = cfg.user; 99 + home = cfg.dataDir; 100 + createHome = true; 101 + }; 102 + users.groups.${cfg.user} = { }; 103 + 104 + networking.firewall.allowedTCPPorts = 105 + lib.mkIf cfg.openFirewall [ cfg.port ]; 106 + 107 + systemd.services.nightshade = { 108 + description = "Nightshade e-reader proxy"; 109 + wantedBy = [ "multi-user.target" ]; 110 + after = [ "network.target" ]; 111 + environment = { 112 + NIGHTSHADE_PORT = toString cfg.port; 113 + NIGHTSHADE_DB = "${cfg.dataDir}/nightshade.sqlite3"; 114 + MINIFLUX_URL = cfg.minifluxUrl; 115 + }; 116 + serviceConfig = { 117 + ExecStart = "${pkg}/bin/nightshade"; 118 + User = cfg.user; 119 + Group = cfg.user; 120 + WorkingDirectory = cfg.dataDir; 121 + Restart = "on-failure"; 122 + RestartSec = 5; 123 + LoadCredential = [ "miniflux_token:${cfg.minifluxTokenFile}" ]; 124 + Environment = [ 125 + "MINIFLUX_TOKEN_FILE=%d/miniflux_token" 126 + ]; 127 + 128 + NoNewPrivileges = true; 129 + PrivateTmp = true; 130 + ProtectSystem = "strict"; 131 + ProtectHome = true; 132 + ReadWritePaths = [ cfg.dataDir ]; 133 + ProtectKernelTunables = true; 134 + ProtectKernelModules = true; 135 + ProtectControlGroups = true; 136 + RestrictNamespaces = true; 137 + RestrictRealtime = true; 138 + LockPersonality = true; 139 + }; 140 + }; 141 + }; 142 + }; 143 + }; 144 + }
+39
package.json
··· 1 + { 2 + "name": "nightshade", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev:server": "tsx watch src/server/index.ts", 8 + "dev:web": "vite", 9 + "dev": "concurrently -n server,web -c blue,green \"pnpm dev:server\" \"pnpm dev:web\"", 10 + "build:web": "vite build", 11 + "build": "pnpm build:web", 12 + "start": "tsx src/server/index.ts", 13 + "typecheck": "tsc --noEmit" 14 + }, 15 + "dependencies": { 16 + "@hono/node-server": "^1.13.7", 17 + "@mozilla/readability": "^0.5.0", 18 + "better-sqlite3": "^11.7.0", 19 + "dotenv": "^17.4.2", 20 + "hono": "^4.6.13", 21 + "linkedom": "^0.18.5", 22 + "preact": "^10.25.1" 23 + }, 24 + "devDependencies": { 25 + "@preact/preset-vite": "^2.9.1", 26 + "@types/better-sqlite3": "^7.6.12", 27 + "@types/node": "^22.10.1", 28 + "concurrently": "^9.1.0", 29 + "tsx": "^4.19.2", 30 + "typescript": "^5.7.2", 31 + "vite": "^6.0.3" 32 + }, 33 + "pnpm": { 34 + "onlyBuiltDependencies": [ 35 + "better-sqlite3", 36 + "esbuild" 37 + ] 38 + } 39 + }
+2162
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@hono/node-server': 12 + specifier: ^1.13.7 13 + version: 1.19.14(hono@4.12.14) 14 + '@mozilla/readability': 15 + specifier: ^0.5.0 16 + version: 0.5.0 17 + better-sqlite3: 18 + specifier: ^11.7.0 19 + version: 11.10.0 20 + dotenv: 21 + specifier: ^17.4.2 22 + version: 17.4.2 23 + hono: 24 + specifier: ^4.6.13 25 + version: 4.12.14 26 + linkedom: 27 + specifier: ^0.18.5 28 + version: 0.18.12 29 + preact: 30 + specifier: ^10.25.1 31 + version: 10.29.1 32 + devDependencies: 33 + '@preact/preset-vite': 34 + specifier: ^2.9.1 35 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.2)(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)) 36 + '@types/better-sqlite3': 37 + specifier: ^7.6.12 38 + version: 7.6.13 39 + '@types/node': 40 + specifier: ^22.10.1 41 + version: 22.19.17 42 + concurrently: 43 + specifier: ^9.1.0 44 + version: 9.2.1 45 + tsx: 46 + specifier: ^4.19.2 47 + version: 4.21.0 48 + typescript: 49 + specifier: ^5.7.2 50 + version: 5.9.3 51 + vite: 52 + specifier: ^6.0.3 53 + version: 6.4.2(@types/node@22.19.17)(tsx@4.21.0) 54 + 55 + packages: 56 + 57 + '@babel/code-frame@7.29.0': 58 + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} 59 + engines: {node: '>=6.9.0'} 60 + 61 + '@babel/compat-data@7.29.0': 62 + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} 63 + engines: {node: '>=6.9.0'} 64 + 65 + '@babel/core@7.29.0': 66 + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} 67 + engines: {node: '>=6.9.0'} 68 + 69 + '@babel/generator@7.29.1': 70 + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} 71 + engines: {node: '>=6.9.0'} 72 + 73 + '@babel/helper-annotate-as-pure@7.27.3': 74 + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} 75 + engines: {node: '>=6.9.0'} 76 + 77 + '@babel/helper-compilation-targets@7.28.6': 78 + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} 79 + engines: {node: '>=6.9.0'} 80 + 81 + '@babel/helper-globals@7.28.0': 82 + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 83 + engines: {node: '>=6.9.0'} 84 + 85 + '@babel/helper-module-imports@7.28.6': 86 + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} 87 + engines: {node: '>=6.9.0'} 88 + 89 + '@babel/helper-module-transforms@7.28.6': 90 + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} 91 + engines: {node: '>=6.9.0'} 92 + peerDependencies: 93 + '@babel/core': ^7.0.0 94 + 95 + '@babel/helper-plugin-utils@7.28.6': 96 + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} 97 + engines: {node: '>=6.9.0'} 98 + 99 + '@babel/helper-string-parser@7.27.1': 100 + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 101 + engines: {node: '>=6.9.0'} 102 + 103 + '@babel/helper-validator-identifier@7.28.5': 104 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 105 + engines: {node: '>=6.9.0'} 106 + 107 + '@babel/helper-validator-option@7.27.1': 108 + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 109 + engines: {node: '>=6.9.0'} 110 + 111 + '@babel/helpers@7.29.2': 112 + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} 113 + engines: {node: '>=6.9.0'} 114 + 115 + '@babel/parser@7.29.2': 116 + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} 117 + engines: {node: '>=6.0.0'} 118 + hasBin: true 119 + 120 + '@babel/plugin-syntax-jsx@7.28.6': 121 + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} 122 + engines: {node: '>=6.9.0'} 123 + peerDependencies: 124 + '@babel/core': ^7.0.0-0 125 + 126 + '@babel/plugin-transform-react-jsx-development@7.27.1': 127 + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} 128 + engines: {node: '>=6.9.0'} 129 + peerDependencies: 130 + '@babel/core': ^7.0.0-0 131 + 132 + '@babel/plugin-transform-react-jsx@7.28.6': 133 + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} 134 + engines: {node: '>=6.9.0'} 135 + peerDependencies: 136 + '@babel/core': ^7.0.0-0 137 + 138 + '@babel/template@7.28.6': 139 + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} 140 + engines: {node: '>=6.9.0'} 141 + 142 + '@babel/traverse@7.29.0': 143 + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} 144 + engines: {node: '>=6.9.0'} 145 + 146 + '@babel/types@7.29.0': 147 + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} 148 + engines: {node: '>=6.9.0'} 149 + 150 + '@esbuild/aix-ppc64@0.25.12': 151 + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 152 + engines: {node: '>=18'} 153 + cpu: [ppc64] 154 + os: [aix] 155 + 156 + '@esbuild/aix-ppc64@0.27.7': 157 + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} 158 + engines: {node: '>=18'} 159 + cpu: [ppc64] 160 + os: [aix] 161 + 162 + '@esbuild/android-arm64@0.25.12': 163 + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 164 + engines: {node: '>=18'} 165 + cpu: [arm64] 166 + os: [android] 167 + 168 + '@esbuild/android-arm64@0.27.7': 169 + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} 170 + engines: {node: '>=18'} 171 + cpu: [arm64] 172 + os: [android] 173 + 174 + '@esbuild/android-arm@0.25.12': 175 + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 176 + engines: {node: '>=18'} 177 + cpu: [arm] 178 + os: [android] 179 + 180 + '@esbuild/android-arm@0.27.7': 181 + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} 182 + engines: {node: '>=18'} 183 + cpu: [arm] 184 + os: [android] 185 + 186 + '@esbuild/android-x64@0.25.12': 187 + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 188 + engines: {node: '>=18'} 189 + cpu: [x64] 190 + os: [android] 191 + 192 + '@esbuild/android-x64@0.27.7': 193 + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} 194 + engines: {node: '>=18'} 195 + cpu: [x64] 196 + os: [android] 197 + 198 + '@esbuild/darwin-arm64@0.25.12': 199 + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 200 + engines: {node: '>=18'} 201 + cpu: [arm64] 202 + os: [darwin] 203 + 204 + '@esbuild/darwin-arm64@0.27.7': 205 + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} 206 + engines: {node: '>=18'} 207 + cpu: [arm64] 208 + os: [darwin] 209 + 210 + '@esbuild/darwin-x64@0.25.12': 211 + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 212 + engines: {node: '>=18'} 213 + cpu: [x64] 214 + os: [darwin] 215 + 216 + '@esbuild/darwin-x64@0.27.7': 217 + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} 218 + engines: {node: '>=18'} 219 + cpu: [x64] 220 + os: [darwin] 221 + 222 + '@esbuild/freebsd-arm64@0.25.12': 223 + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 224 + engines: {node: '>=18'} 225 + cpu: [arm64] 226 + os: [freebsd] 227 + 228 + '@esbuild/freebsd-arm64@0.27.7': 229 + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} 230 + engines: {node: '>=18'} 231 + cpu: [arm64] 232 + os: [freebsd] 233 + 234 + '@esbuild/freebsd-x64@0.25.12': 235 + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 236 + engines: {node: '>=18'} 237 + cpu: [x64] 238 + os: [freebsd] 239 + 240 + '@esbuild/freebsd-x64@0.27.7': 241 + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} 242 + engines: {node: '>=18'} 243 + cpu: [x64] 244 + os: [freebsd] 245 + 246 + '@esbuild/linux-arm64@0.25.12': 247 + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 248 + engines: {node: '>=18'} 249 + cpu: [arm64] 250 + os: [linux] 251 + 252 + '@esbuild/linux-arm64@0.27.7': 253 + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} 254 + engines: {node: '>=18'} 255 + cpu: [arm64] 256 + os: [linux] 257 + 258 + '@esbuild/linux-arm@0.25.12': 259 + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 260 + engines: {node: '>=18'} 261 + cpu: [arm] 262 + os: [linux] 263 + 264 + '@esbuild/linux-arm@0.27.7': 265 + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} 266 + engines: {node: '>=18'} 267 + cpu: [arm] 268 + os: [linux] 269 + 270 + '@esbuild/linux-ia32@0.25.12': 271 + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 272 + engines: {node: '>=18'} 273 + cpu: [ia32] 274 + os: [linux] 275 + 276 + '@esbuild/linux-ia32@0.27.7': 277 + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} 278 + engines: {node: '>=18'} 279 + cpu: [ia32] 280 + os: [linux] 281 + 282 + '@esbuild/linux-loong64@0.25.12': 283 + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 284 + engines: {node: '>=18'} 285 + cpu: [loong64] 286 + os: [linux] 287 + 288 + '@esbuild/linux-loong64@0.27.7': 289 + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} 290 + engines: {node: '>=18'} 291 + cpu: [loong64] 292 + os: [linux] 293 + 294 + '@esbuild/linux-mips64el@0.25.12': 295 + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 296 + engines: {node: '>=18'} 297 + cpu: [mips64el] 298 + os: [linux] 299 + 300 + '@esbuild/linux-mips64el@0.27.7': 301 + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} 302 + engines: {node: '>=18'} 303 + cpu: [mips64el] 304 + os: [linux] 305 + 306 + '@esbuild/linux-ppc64@0.25.12': 307 + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 308 + engines: {node: '>=18'} 309 + cpu: [ppc64] 310 + os: [linux] 311 + 312 + '@esbuild/linux-ppc64@0.27.7': 313 + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} 314 + engines: {node: '>=18'} 315 + cpu: [ppc64] 316 + os: [linux] 317 + 318 + '@esbuild/linux-riscv64@0.25.12': 319 + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 320 + engines: {node: '>=18'} 321 + cpu: [riscv64] 322 + os: [linux] 323 + 324 + '@esbuild/linux-riscv64@0.27.7': 325 + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} 326 + engines: {node: '>=18'} 327 + cpu: [riscv64] 328 + os: [linux] 329 + 330 + '@esbuild/linux-s390x@0.25.12': 331 + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 332 + engines: {node: '>=18'} 333 + cpu: [s390x] 334 + os: [linux] 335 + 336 + '@esbuild/linux-s390x@0.27.7': 337 + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} 338 + engines: {node: '>=18'} 339 + cpu: [s390x] 340 + os: [linux] 341 + 342 + '@esbuild/linux-x64@0.25.12': 343 + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 344 + engines: {node: '>=18'} 345 + cpu: [x64] 346 + os: [linux] 347 + 348 + '@esbuild/linux-x64@0.27.7': 349 + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} 350 + engines: {node: '>=18'} 351 + cpu: [x64] 352 + os: [linux] 353 + 354 + '@esbuild/netbsd-arm64@0.25.12': 355 + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 356 + engines: {node: '>=18'} 357 + cpu: [arm64] 358 + os: [netbsd] 359 + 360 + '@esbuild/netbsd-arm64@0.27.7': 361 + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} 362 + engines: {node: '>=18'} 363 + cpu: [arm64] 364 + os: [netbsd] 365 + 366 + '@esbuild/netbsd-x64@0.25.12': 367 + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 368 + engines: {node: '>=18'} 369 + cpu: [x64] 370 + os: [netbsd] 371 + 372 + '@esbuild/netbsd-x64@0.27.7': 373 + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} 374 + engines: {node: '>=18'} 375 + cpu: [x64] 376 + os: [netbsd] 377 + 378 + '@esbuild/openbsd-arm64@0.25.12': 379 + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 380 + engines: {node: '>=18'} 381 + cpu: [arm64] 382 + os: [openbsd] 383 + 384 + '@esbuild/openbsd-arm64@0.27.7': 385 + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} 386 + engines: {node: '>=18'} 387 + cpu: [arm64] 388 + os: [openbsd] 389 + 390 + '@esbuild/openbsd-x64@0.25.12': 391 + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 392 + engines: {node: '>=18'} 393 + cpu: [x64] 394 + os: [openbsd] 395 + 396 + '@esbuild/openbsd-x64@0.27.7': 397 + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} 398 + engines: {node: '>=18'} 399 + cpu: [x64] 400 + os: [openbsd] 401 + 402 + '@esbuild/openharmony-arm64@0.25.12': 403 + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 404 + engines: {node: '>=18'} 405 + cpu: [arm64] 406 + os: [openharmony] 407 + 408 + '@esbuild/openharmony-arm64@0.27.7': 409 + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} 410 + engines: {node: '>=18'} 411 + cpu: [arm64] 412 + os: [openharmony] 413 + 414 + '@esbuild/sunos-x64@0.25.12': 415 + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 416 + engines: {node: '>=18'} 417 + cpu: [x64] 418 + os: [sunos] 419 + 420 + '@esbuild/sunos-x64@0.27.7': 421 + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} 422 + engines: {node: '>=18'} 423 + cpu: [x64] 424 + os: [sunos] 425 + 426 + '@esbuild/win32-arm64@0.25.12': 427 + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 428 + engines: {node: '>=18'} 429 + cpu: [arm64] 430 + os: [win32] 431 + 432 + '@esbuild/win32-arm64@0.27.7': 433 + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} 434 + engines: {node: '>=18'} 435 + cpu: [arm64] 436 + os: [win32] 437 + 438 + '@esbuild/win32-ia32@0.25.12': 439 + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 440 + engines: {node: '>=18'} 441 + cpu: [ia32] 442 + os: [win32] 443 + 444 + '@esbuild/win32-ia32@0.27.7': 445 + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} 446 + engines: {node: '>=18'} 447 + cpu: [ia32] 448 + os: [win32] 449 + 450 + '@esbuild/win32-x64@0.25.12': 451 + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 452 + engines: {node: '>=18'} 453 + cpu: [x64] 454 + os: [win32] 455 + 456 + '@esbuild/win32-x64@0.27.7': 457 + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} 458 + engines: {node: '>=18'} 459 + cpu: [x64] 460 + os: [win32] 461 + 462 + '@hono/node-server@1.19.14': 463 + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} 464 + engines: {node: '>=18.14.1'} 465 + peerDependencies: 466 + hono: ^4 467 + 468 + '@jridgewell/gen-mapping@0.3.13': 469 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 470 + 471 + '@jridgewell/remapping@2.3.5': 472 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 473 + 474 + '@jridgewell/resolve-uri@3.1.2': 475 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 476 + engines: {node: '>=6.0.0'} 477 + 478 + '@jridgewell/sourcemap-codec@1.5.5': 479 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 480 + 481 + '@jridgewell/trace-mapping@0.3.31': 482 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 483 + 484 + '@mozilla/readability@0.5.0': 485 + resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==} 486 + engines: {node: '>=14.0.0'} 487 + 488 + '@preact/preset-vite@2.10.5': 489 + resolution: {integrity: sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==} 490 + peerDependencies: 491 + '@babel/core': 7.x 492 + vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x 493 + 494 + '@prefresh/babel-plugin@0.5.3': 495 + resolution: {integrity: sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==} 496 + 497 + '@prefresh/core@1.5.9': 498 + resolution: {integrity: sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==} 499 + peerDependencies: 500 + preact: ^10.0.0 || ^11.0.0-0 501 + 502 + '@prefresh/utils@1.2.1': 503 + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} 504 + 505 + '@prefresh/vite@2.4.12': 506 + resolution: {integrity: sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==} 507 + peerDependencies: 508 + preact: ^10.4.0 || ^11.0.0-0 509 + vite: '>=2.0.0' 510 + 511 + '@rollup/pluginutils@4.2.1': 512 + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} 513 + engines: {node: '>= 8.0.0'} 514 + 515 + '@rollup/pluginutils@5.3.0': 516 + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} 517 + engines: {node: '>=14.0.0'} 518 + peerDependencies: 519 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 520 + peerDependenciesMeta: 521 + rollup: 522 + optional: true 523 + 524 + '@rollup/rollup-android-arm-eabi@4.60.2': 525 + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} 526 + cpu: [arm] 527 + os: [android] 528 + 529 + '@rollup/rollup-android-arm64@4.60.2': 530 + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} 531 + cpu: [arm64] 532 + os: [android] 533 + 534 + '@rollup/rollup-darwin-arm64@4.60.2': 535 + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} 536 + cpu: [arm64] 537 + os: [darwin] 538 + 539 + '@rollup/rollup-darwin-x64@4.60.2': 540 + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} 541 + cpu: [x64] 542 + os: [darwin] 543 + 544 + '@rollup/rollup-freebsd-arm64@4.60.2': 545 + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} 546 + cpu: [arm64] 547 + os: [freebsd] 548 + 549 + '@rollup/rollup-freebsd-x64@4.60.2': 550 + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} 551 + cpu: [x64] 552 + os: [freebsd] 553 + 554 + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': 555 + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} 556 + cpu: [arm] 557 + os: [linux] 558 + libc: [glibc] 559 + 560 + '@rollup/rollup-linux-arm-musleabihf@4.60.2': 561 + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} 562 + cpu: [arm] 563 + os: [linux] 564 + libc: [musl] 565 + 566 + '@rollup/rollup-linux-arm64-gnu@4.60.2': 567 + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} 568 + cpu: [arm64] 569 + os: [linux] 570 + libc: [glibc] 571 + 572 + '@rollup/rollup-linux-arm64-musl@4.60.2': 573 + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} 574 + cpu: [arm64] 575 + os: [linux] 576 + libc: [musl] 577 + 578 + '@rollup/rollup-linux-loong64-gnu@4.60.2': 579 + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} 580 + cpu: [loong64] 581 + os: [linux] 582 + libc: [glibc] 583 + 584 + '@rollup/rollup-linux-loong64-musl@4.60.2': 585 + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} 586 + cpu: [loong64] 587 + os: [linux] 588 + libc: [musl] 589 + 590 + '@rollup/rollup-linux-ppc64-gnu@4.60.2': 591 + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} 592 + cpu: [ppc64] 593 + os: [linux] 594 + libc: [glibc] 595 + 596 + '@rollup/rollup-linux-ppc64-musl@4.60.2': 597 + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} 598 + cpu: [ppc64] 599 + os: [linux] 600 + libc: [musl] 601 + 602 + '@rollup/rollup-linux-riscv64-gnu@4.60.2': 603 + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} 604 + cpu: [riscv64] 605 + os: [linux] 606 + libc: [glibc] 607 + 608 + '@rollup/rollup-linux-riscv64-musl@4.60.2': 609 + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} 610 + cpu: [riscv64] 611 + os: [linux] 612 + libc: [musl] 613 + 614 + '@rollup/rollup-linux-s390x-gnu@4.60.2': 615 + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} 616 + cpu: [s390x] 617 + os: [linux] 618 + libc: [glibc] 619 + 620 + '@rollup/rollup-linux-x64-gnu@4.60.2': 621 + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} 622 + cpu: [x64] 623 + os: [linux] 624 + libc: [glibc] 625 + 626 + '@rollup/rollup-linux-x64-musl@4.60.2': 627 + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} 628 + cpu: [x64] 629 + os: [linux] 630 + libc: [musl] 631 + 632 + '@rollup/rollup-openbsd-x64@4.60.2': 633 + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} 634 + cpu: [x64] 635 + os: [openbsd] 636 + 637 + '@rollup/rollup-openharmony-arm64@4.60.2': 638 + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} 639 + cpu: [arm64] 640 + os: [openharmony] 641 + 642 + '@rollup/rollup-win32-arm64-msvc@4.60.2': 643 + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} 644 + cpu: [arm64] 645 + os: [win32] 646 + 647 + '@rollup/rollup-win32-ia32-msvc@4.60.2': 648 + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} 649 + cpu: [ia32] 650 + os: [win32] 651 + 652 + '@rollup/rollup-win32-x64-gnu@4.60.2': 653 + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} 654 + cpu: [x64] 655 + os: [win32] 656 + 657 + '@rollup/rollup-win32-x64-msvc@4.60.2': 658 + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} 659 + cpu: [x64] 660 + os: [win32] 661 + 662 + '@types/better-sqlite3@7.6.13': 663 + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} 664 + 665 + '@types/estree@1.0.8': 666 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 667 + 668 + '@types/node@22.19.17': 669 + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} 670 + 671 + ansi-regex@5.0.1: 672 + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 673 + engines: {node: '>=8'} 674 + 675 + ansi-styles@4.3.0: 676 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 677 + engines: {node: '>=8'} 678 + 679 + babel-plugin-transform-hook-names@1.0.2: 680 + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} 681 + peerDependencies: 682 + '@babel/core': ^7.12.10 683 + 684 + base64-js@1.5.1: 685 + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 686 + 687 + baseline-browser-mapping@2.10.20: 688 + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} 689 + engines: {node: '>=6.0.0'} 690 + hasBin: true 691 + 692 + better-sqlite3@11.10.0: 693 + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} 694 + 695 + bindings@1.5.0: 696 + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 697 + 698 + bl@4.1.0: 699 + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} 700 + 701 + boolbase@1.0.0: 702 + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 703 + 704 + browserslist@4.28.2: 705 + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} 706 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 707 + hasBin: true 708 + 709 + buffer@5.7.1: 710 + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} 711 + 712 + caniuse-lite@1.0.30001788: 713 + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} 714 + 715 + chalk@4.1.2: 716 + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 717 + engines: {node: '>=10'} 718 + 719 + chownr@1.1.4: 720 + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} 721 + 722 + cliui@8.0.1: 723 + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 724 + engines: {node: '>=12'} 725 + 726 + color-convert@2.0.1: 727 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 728 + engines: {node: '>=7.0.0'} 729 + 730 + color-name@1.1.4: 731 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 732 + 733 + concurrently@9.2.1: 734 + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} 735 + engines: {node: '>=18'} 736 + hasBin: true 737 + 738 + convert-source-map@2.0.0: 739 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 740 + 741 + css-select@5.2.2: 742 + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} 743 + 744 + css-what@6.2.2: 745 + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} 746 + engines: {node: '>= 6'} 747 + 748 + cssom@0.5.0: 749 + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} 750 + 751 + debug@4.4.3: 752 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 753 + engines: {node: '>=6.0'} 754 + peerDependencies: 755 + supports-color: '*' 756 + peerDependenciesMeta: 757 + supports-color: 758 + optional: true 759 + 760 + decompress-response@6.0.0: 761 + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} 762 + engines: {node: '>=10'} 763 + 764 + deep-extend@0.6.0: 765 + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} 766 + engines: {node: '>=4.0.0'} 767 + 768 + detect-libc@2.1.2: 769 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 770 + engines: {node: '>=8'} 771 + 772 + dom-serializer@2.0.0: 773 + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 774 + 775 + domelementtype@2.3.0: 776 + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 777 + 778 + domhandler@5.0.3: 779 + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 780 + engines: {node: '>= 4'} 781 + 782 + domutils@3.2.2: 783 + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 784 + 785 + dotenv@17.4.2: 786 + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} 787 + engines: {node: '>=12'} 788 + 789 + electron-to-chromium@1.5.340: 790 + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} 791 + 792 + emoji-regex@8.0.0: 793 + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 794 + 795 + end-of-stream@1.4.5: 796 + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} 797 + 798 + entities@4.5.0: 799 + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 800 + engines: {node: '>=0.12'} 801 + 802 + entities@7.0.1: 803 + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} 804 + engines: {node: '>=0.12'} 805 + 806 + esbuild@0.25.12: 807 + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 808 + engines: {node: '>=18'} 809 + hasBin: true 810 + 811 + esbuild@0.27.7: 812 + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} 813 + engines: {node: '>=18'} 814 + hasBin: true 815 + 816 + escalade@3.2.0: 817 + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 818 + engines: {node: '>=6'} 819 + 820 + estree-walker@2.0.2: 821 + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 822 + 823 + expand-template@2.0.3: 824 + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} 825 + engines: {node: '>=6'} 826 + 827 + fdir@6.5.0: 828 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 829 + engines: {node: '>=12.0.0'} 830 + peerDependencies: 831 + picomatch: ^3 || ^4 832 + peerDependenciesMeta: 833 + picomatch: 834 + optional: true 835 + 836 + file-uri-to-path@1.0.0: 837 + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} 838 + 839 + fs-constants@1.0.0: 840 + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} 841 + 842 + fsevents@2.3.3: 843 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 844 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 845 + os: [darwin] 846 + 847 + gensync@1.0.0-beta.2: 848 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 849 + engines: {node: '>=6.9.0'} 850 + 851 + get-caller-file@2.0.5: 852 + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 853 + engines: {node: 6.* || 8.* || >= 10.*} 854 + 855 + get-tsconfig@4.14.0: 856 + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} 857 + 858 + github-from-package@0.0.0: 859 + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} 860 + 861 + has-flag@4.0.0: 862 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 863 + engines: {node: '>=8'} 864 + 865 + he@1.2.0: 866 + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 867 + hasBin: true 868 + 869 + hono@4.12.14: 870 + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} 871 + engines: {node: '>=16.9.0'} 872 + 873 + html-escaper@3.0.3: 874 + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} 875 + 876 + htmlparser2@10.1.0: 877 + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} 878 + 879 + ieee754@1.2.1: 880 + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 881 + 882 + inherits@2.0.4: 883 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 884 + 885 + ini@1.3.8: 886 + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} 887 + 888 + is-fullwidth-code-point@3.0.0: 889 + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 890 + engines: {node: '>=8'} 891 + 892 + js-tokens@4.0.0: 893 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 894 + 895 + jsesc@3.1.0: 896 + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 897 + engines: {node: '>=6'} 898 + hasBin: true 899 + 900 + json5@2.2.3: 901 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 902 + engines: {node: '>=6'} 903 + hasBin: true 904 + 905 + kolorist@1.8.0: 906 + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} 907 + 908 + linkedom@0.18.12: 909 + resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} 910 + engines: {node: '>=16'} 911 + peerDependencies: 912 + canvas: '>= 2' 913 + peerDependenciesMeta: 914 + canvas: 915 + optional: true 916 + 917 + lru-cache@5.1.1: 918 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 919 + 920 + magic-string@0.30.21: 921 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 922 + 923 + mimic-response@3.1.0: 924 + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} 925 + engines: {node: '>=10'} 926 + 927 + minimist@1.2.8: 928 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 929 + 930 + mkdirp-classic@0.5.3: 931 + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} 932 + 933 + ms@2.1.3: 934 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 935 + 936 + nanoid@3.3.11: 937 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 938 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 939 + hasBin: true 940 + 941 + napi-build-utils@2.0.0: 942 + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} 943 + 944 + node-abi@3.89.0: 945 + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} 946 + engines: {node: '>=10'} 947 + 948 + node-html-parser@6.1.13: 949 + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} 950 + 951 + node-releases@2.0.37: 952 + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} 953 + 954 + nth-check@2.1.1: 955 + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 956 + 957 + once@1.4.0: 958 + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 959 + 960 + picocolors@1.1.1: 961 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 962 + 963 + picomatch@2.3.2: 964 + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} 965 + engines: {node: '>=8.6'} 966 + 967 + picomatch@4.0.4: 968 + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} 969 + engines: {node: '>=12'} 970 + 971 + postcss@8.5.10: 972 + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} 973 + engines: {node: ^10 || ^12 || >=14} 974 + 975 + preact@10.29.1: 976 + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} 977 + 978 + prebuild-install@7.1.3: 979 + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} 980 + engines: {node: '>=10'} 981 + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. 982 + hasBin: true 983 + 984 + pump@3.0.4: 985 + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} 986 + 987 + rc@1.2.8: 988 + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} 989 + hasBin: true 990 + 991 + readable-stream@3.6.2: 992 + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 993 + engines: {node: '>= 6'} 994 + 995 + require-directory@2.1.1: 996 + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 997 + engines: {node: '>=0.10.0'} 998 + 999 + resolve-pkg-maps@1.0.0: 1000 + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1001 + 1002 + rollup@4.60.2: 1003 + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} 1004 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1005 + hasBin: true 1006 + 1007 + rxjs@7.8.2: 1008 + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 1009 + 1010 + safe-buffer@5.2.1: 1011 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1012 + 1013 + semver@6.3.1: 1014 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1015 + hasBin: true 1016 + 1017 + semver@7.7.4: 1018 + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} 1019 + engines: {node: '>=10'} 1020 + hasBin: true 1021 + 1022 + shell-quote@1.8.3: 1023 + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} 1024 + engines: {node: '>= 0.4'} 1025 + 1026 + simple-code-frame@1.3.0: 1027 + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} 1028 + 1029 + simple-concat@1.0.1: 1030 + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} 1031 + 1032 + simple-get@4.0.1: 1033 + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} 1034 + 1035 + source-map-js@1.2.1: 1036 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1037 + engines: {node: '>=0.10.0'} 1038 + 1039 + source-map@0.7.6: 1040 + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} 1041 + engines: {node: '>= 12'} 1042 + 1043 + stack-trace@1.0.0-pre2: 1044 + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} 1045 + engines: {node: '>=16'} 1046 + 1047 + string-width@4.2.3: 1048 + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1049 + engines: {node: '>=8'} 1050 + 1051 + string_decoder@1.3.0: 1052 + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1053 + 1054 + strip-ansi@6.0.1: 1055 + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1056 + engines: {node: '>=8'} 1057 + 1058 + strip-json-comments@2.0.1: 1059 + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 1060 + engines: {node: '>=0.10.0'} 1061 + 1062 + supports-color@7.2.0: 1063 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1064 + engines: {node: '>=8'} 1065 + 1066 + supports-color@8.1.1: 1067 + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 1068 + engines: {node: '>=10'} 1069 + 1070 + tar-fs@2.1.4: 1071 + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} 1072 + 1073 + tar-stream@2.2.0: 1074 + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} 1075 + engines: {node: '>=6'} 1076 + 1077 + tinyglobby@0.2.16: 1078 + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} 1079 + engines: {node: '>=12.0.0'} 1080 + 1081 + tree-kill@1.2.2: 1082 + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1083 + hasBin: true 1084 + 1085 + tslib@2.8.1: 1086 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1087 + 1088 + tsx@4.21.0: 1089 + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} 1090 + engines: {node: '>=18.0.0'} 1091 + hasBin: true 1092 + 1093 + tunnel-agent@0.6.0: 1094 + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} 1095 + 1096 + typescript@5.9.3: 1097 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1098 + engines: {node: '>=14.17'} 1099 + hasBin: true 1100 + 1101 + uhyphen@0.2.0: 1102 + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} 1103 + 1104 + undici-types@6.21.0: 1105 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1106 + 1107 + update-browserslist-db@1.2.3: 1108 + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 1109 + hasBin: true 1110 + peerDependencies: 1111 + browserslist: '>= 4.21.0' 1112 + 1113 + util-deprecate@1.0.2: 1114 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1115 + 1116 + vite-prerender-plugin@0.5.13: 1117 + resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==} 1118 + peerDependencies: 1119 + vite: 5.x || 6.x || 7.x || 8.x 1120 + 1121 + vite@6.4.2: 1122 + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} 1123 + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1124 + hasBin: true 1125 + peerDependencies: 1126 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1127 + jiti: '>=1.21.0' 1128 + less: '*' 1129 + lightningcss: ^1.21.0 1130 + sass: '*' 1131 + sass-embedded: '*' 1132 + stylus: '*' 1133 + sugarss: '*' 1134 + terser: ^5.16.0 1135 + tsx: ^4.8.1 1136 + yaml: ^2.4.2 1137 + peerDependenciesMeta: 1138 + '@types/node': 1139 + optional: true 1140 + jiti: 1141 + optional: true 1142 + less: 1143 + optional: true 1144 + lightningcss: 1145 + optional: true 1146 + sass: 1147 + optional: true 1148 + sass-embedded: 1149 + optional: true 1150 + stylus: 1151 + optional: true 1152 + sugarss: 1153 + optional: true 1154 + terser: 1155 + optional: true 1156 + tsx: 1157 + optional: true 1158 + yaml: 1159 + optional: true 1160 + 1161 + wrap-ansi@7.0.0: 1162 + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1163 + engines: {node: '>=10'} 1164 + 1165 + wrappy@1.0.2: 1166 + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1167 + 1168 + y18n@5.0.8: 1169 + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 1170 + engines: {node: '>=10'} 1171 + 1172 + yallist@3.1.1: 1173 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1174 + 1175 + yargs-parser@21.1.1: 1176 + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 1177 + engines: {node: '>=12'} 1178 + 1179 + yargs@17.7.2: 1180 + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 1181 + engines: {node: '>=12'} 1182 + 1183 + zimmerframe@1.1.4: 1184 + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 1185 + 1186 + snapshots: 1187 + 1188 + '@babel/code-frame@7.29.0': 1189 + dependencies: 1190 + '@babel/helper-validator-identifier': 7.28.5 1191 + js-tokens: 4.0.0 1192 + picocolors: 1.1.1 1193 + 1194 + '@babel/compat-data@7.29.0': {} 1195 + 1196 + '@babel/core@7.29.0': 1197 + dependencies: 1198 + '@babel/code-frame': 7.29.0 1199 + '@babel/generator': 7.29.1 1200 + '@babel/helper-compilation-targets': 7.28.6 1201 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) 1202 + '@babel/helpers': 7.29.2 1203 + '@babel/parser': 7.29.2 1204 + '@babel/template': 7.28.6 1205 + '@babel/traverse': 7.29.0 1206 + '@babel/types': 7.29.0 1207 + '@jridgewell/remapping': 2.3.5 1208 + convert-source-map: 2.0.0 1209 + debug: 4.4.3 1210 + gensync: 1.0.0-beta.2 1211 + json5: 2.2.3 1212 + semver: 6.3.1 1213 + transitivePeerDependencies: 1214 + - supports-color 1215 + 1216 + '@babel/generator@7.29.1': 1217 + dependencies: 1218 + '@babel/parser': 7.29.2 1219 + '@babel/types': 7.29.0 1220 + '@jridgewell/gen-mapping': 0.3.13 1221 + '@jridgewell/trace-mapping': 0.3.31 1222 + jsesc: 3.1.0 1223 + 1224 + '@babel/helper-annotate-as-pure@7.27.3': 1225 + dependencies: 1226 + '@babel/types': 7.29.0 1227 + 1228 + '@babel/helper-compilation-targets@7.28.6': 1229 + dependencies: 1230 + '@babel/compat-data': 7.29.0 1231 + '@babel/helper-validator-option': 7.27.1 1232 + browserslist: 4.28.2 1233 + lru-cache: 5.1.1 1234 + semver: 6.3.1 1235 + 1236 + '@babel/helper-globals@7.28.0': {} 1237 + 1238 + '@babel/helper-module-imports@7.28.6': 1239 + dependencies: 1240 + '@babel/traverse': 7.29.0 1241 + '@babel/types': 7.29.0 1242 + transitivePeerDependencies: 1243 + - supports-color 1244 + 1245 + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': 1246 + dependencies: 1247 + '@babel/core': 7.29.0 1248 + '@babel/helper-module-imports': 7.28.6 1249 + '@babel/helper-validator-identifier': 7.28.5 1250 + '@babel/traverse': 7.29.0 1251 + transitivePeerDependencies: 1252 + - supports-color 1253 + 1254 + '@babel/helper-plugin-utils@7.28.6': {} 1255 + 1256 + '@babel/helper-string-parser@7.27.1': {} 1257 + 1258 + '@babel/helper-validator-identifier@7.28.5': {} 1259 + 1260 + '@babel/helper-validator-option@7.27.1': {} 1261 + 1262 + '@babel/helpers@7.29.2': 1263 + dependencies: 1264 + '@babel/template': 7.28.6 1265 + '@babel/types': 7.29.0 1266 + 1267 + '@babel/parser@7.29.2': 1268 + dependencies: 1269 + '@babel/types': 7.29.0 1270 + 1271 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': 1272 + dependencies: 1273 + '@babel/core': 7.29.0 1274 + '@babel/helper-plugin-utils': 7.28.6 1275 + 1276 + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': 1277 + dependencies: 1278 + '@babel/core': 7.29.0 1279 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) 1280 + transitivePeerDependencies: 1281 + - supports-color 1282 + 1283 + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': 1284 + dependencies: 1285 + '@babel/core': 7.29.0 1286 + '@babel/helper-annotate-as-pure': 7.27.3 1287 + '@babel/helper-module-imports': 7.28.6 1288 + '@babel/helper-plugin-utils': 7.28.6 1289 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) 1290 + '@babel/types': 7.29.0 1291 + transitivePeerDependencies: 1292 + - supports-color 1293 + 1294 + '@babel/template@7.28.6': 1295 + dependencies: 1296 + '@babel/code-frame': 7.29.0 1297 + '@babel/parser': 7.29.2 1298 + '@babel/types': 7.29.0 1299 + 1300 + '@babel/traverse@7.29.0': 1301 + dependencies: 1302 + '@babel/code-frame': 7.29.0 1303 + '@babel/generator': 7.29.1 1304 + '@babel/helper-globals': 7.28.0 1305 + '@babel/parser': 7.29.2 1306 + '@babel/template': 7.28.6 1307 + '@babel/types': 7.29.0 1308 + debug: 4.4.3 1309 + transitivePeerDependencies: 1310 + - supports-color 1311 + 1312 + '@babel/types@7.29.0': 1313 + dependencies: 1314 + '@babel/helper-string-parser': 7.27.1 1315 + '@babel/helper-validator-identifier': 7.28.5 1316 + 1317 + '@esbuild/aix-ppc64@0.25.12': 1318 + optional: true 1319 + 1320 + '@esbuild/aix-ppc64@0.27.7': 1321 + optional: true 1322 + 1323 + '@esbuild/android-arm64@0.25.12': 1324 + optional: true 1325 + 1326 + '@esbuild/android-arm64@0.27.7': 1327 + optional: true 1328 + 1329 + '@esbuild/android-arm@0.25.12': 1330 + optional: true 1331 + 1332 + '@esbuild/android-arm@0.27.7': 1333 + optional: true 1334 + 1335 + '@esbuild/android-x64@0.25.12': 1336 + optional: true 1337 + 1338 + '@esbuild/android-x64@0.27.7': 1339 + optional: true 1340 + 1341 + '@esbuild/darwin-arm64@0.25.12': 1342 + optional: true 1343 + 1344 + '@esbuild/darwin-arm64@0.27.7': 1345 + optional: true 1346 + 1347 + '@esbuild/darwin-x64@0.25.12': 1348 + optional: true 1349 + 1350 + '@esbuild/darwin-x64@0.27.7': 1351 + optional: true 1352 + 1353 + '@esbuild/freebsd-arm64@0.25.12': 1354 + optional: true 1355 + 1356 + '@esbuild/freebsd-arm64@0.27.7': 1357 + optional: true 1358 + 1359 + '@esbuild/freebsd-x64@0.25.12': 1360 + optional: true 1361 + 1362 + '@esbuild/freebsd-x64@0.27.7': 1363 + optional: true 1364 + 1365 + '@esbuild/linux-arm64@0.25.12': 1366 + optional: true 1367 + 1368 + '@esbuild/linux-arm64@0.27.7': 1369 + optional: true 1370 + 1371 + '@esbuild/linux-arm@0.25.12': 1372 + optional: true 1373 + 1374 + '@esbuild/linux-arm@0.27.7': 1375 + optional: true 1376 + 1377 + '@esbuild/linux-ia32@0.25.12': 1378 + optional: true 1379 + 1380 + '@esbuild/linux-ia32@0.27.7': 1381 + optional: true 1382 + 1383 + '@esbuild/linux-loong64@0.25.12': 1384 + optional: true 1385 + 1386 + '@esbuild/linux-loong64@0.27.7': 1387 + optional: true 1388 + 1389 + '@esbuild/linux-mips64el@0.25.12': 1390 + optional: true 1391 + 1392 + '@esbuild/linux-mips64el@0.27.7': 1393 + optional: true 1394 + 1395 + '@esbuild/linux-ppc64@0.25.12': 1396 + optional: true 1397 + 1398 + '@esbuild/linux-ppc64@0.27.7': 1399 + optional: true 1400 + 1401 + '@esbuild/linux-riscv64@0.25.12': 1402 + optional: true 1403 + 1404 + '@esbuild/linux-riscv64@0.27.7': 1405 + optional: true 1406 + 1407 + '@esbuild/linux-s390x@0.25.12': 1408 + optional: true 1409 + 1410 + '@esbuild/linux-s390x@0.27.7': 1411 + optional: true 1412 + 1413 + '@esbuild/linux-x64@0.25.12': 1414 + optional: true 1415 + 1416 + '@esbuild/linux-x64@0.27.7': 1417 + optional: true 1418 + 1419 + '@esbuild/netbsd-arm64@0.25.12': 1420 + optional: true 1421 + 1422 + '@esbuild/netbsd-arm64@0.27.7': 1423 + optional: true 1424 + 1425 + '@esbuild/netbsd-x64@0.25.12': 1426 + optional: true 1427 + 1428 + '@esbuild/netbsd-x64@0.27.7': 1429 + optional: true 1430 + 1431 + '@esbuild/openbsd-arm64@0.25.12': 1432 + optional: true 1433 + 1434 + '@esbuild/openbsd-arm64@0.27.7': 1435 + optional: true 1436 + 1437 + '@esbuild/openbsd-x64@0.25.12': 1438 + optional: true 1439 + 1440 + '@esbuild/openbsd-x64@0.27.7': 1441 + optional: true 1442 + 1443 + '@esbuild/openharmony-arm64@0.25.12': 1444 + optional: true 1445 + 1446 + '@esbuild/openharmony-arm64@0.27.7': 1447 + optional: true 1448 + 1449 + '@esbuild/sunos-x64@0.25.12': 1450 + optional: true 1451 + 1452 + '@esbuild/sunos-x64@0.27.7': 1453 + optional: true 1454 + 1455 + '@esbuild/win32-arm64@0.25.12': 1456 + optional: true 1457 + 1458 + '@esbuild/win32-arm64@0.27.7': 1459 + optional: true 1460 + 1461 + '@esbuild/win32-ia32@0.25.12': 1462 + optional: true 1463 + 1464 + '@esbuild/win32-ia32@0.27.7': 1465 + optional: true 1466 + 1467 + '@esbuild/win32-x64@0.25.12': 1468 + optional: true 1469 + 1470 + '@esbuild/win32-x64@0.27.7': 1471 + optional: true 1472 + 1473 + '@hono/node-server@1.19.14(hono@4.12.14)': 1474 + dependencies: 1475 + hono: 4.12.14 1476 + 1477 + '@jridgewell/gen-mapping@0.3.13': 1478 + dependencies: 1479 + '@jridgewell/sourcemap-codec': 1.5.5 1480 + '@jridgewell/trace-mapping': 0.3.31 1481 + 1482 + '@jridgewell/remapping@2.3.5': 1483 + dependencies: 1484 + '@jridgewell/gen-mapping': 0.3.13 1485 + '@jridgewell/trace-mapping': 0.3.31 1486 + 1487 + '@jridgewell/resolve-uri@3.1.2': {} 1488 + 1489 + '@jridgewell/sourcemap-codec@1.5.5': {} 1490 + 1491 + '@jridgewell/trace-mapping@0.3.31': 1492 + dependencies: 1493 + '@jridgewell/resolve-uri': 3.1.2 1494 + '@jridgewell/sourcemap-codec': 1.5.5 1495 + 1496 + '@mozilla/readability@0.5.0': {} 1497 + 1498 + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.2)(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0))': 1499 + dependencies: 1500 + '@babel/core': 7.29.0 1501 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) 1502 + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) 1503 + '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)) 1504 + '@rollup/pluginutils': 5.3.0(rollup@4.60.2) 1505 + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) 1506 + debug: 4.4.3 1507 + magic-string: 0.30.21 1508 + picocolors: 1.1.1 1509 + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0) 1510 + vite-prerender-plugin: 0.5.13(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)) 1511 + zimmerframe: 1.1.4 1512 + transitivePeerDependencies: 1513 + - preact 1514 + - rollup 1515 + - supports-color 1516 + 1517 + '@prefresh/babel-plugin@0.5.3': {} 1518 + 1519 + '@prefresh/core@1.5.9(preact@10.29.1)': 1520 + dependencies: 1521 + preact: 10.29.1 1522 + 1523 + '@prefresh/utils@1.2.1': {} 1524 + 1525 + '@prefresh/vite@2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0))': 1526 + dependencies: 1527 + '@babel/core': 7.29.0 1528 + '@prefresh/babel-plugin': 0.5.3 1529 + '@prefresh/core': 1.5.9(preact@10.29.1) 1530 + '@prefresh/utils': 1.2.1 1531 + '@rollup/pluginutils': 4.2.1 1532 + preact: 10.29.1 1533 + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0) 1534 + transitivePeerDependencies: 1535 + - supports-color 1536 + 1537 + '@rollup/pluginutils@4.2.1': 1538 + dependencies: 1539 + estree-walker: 2.0.2 1540 + picomatch: 2.3.2 1541 + 1542 + '@rollup/pluginutils@5.3.0(rollup@4.60.2)': 1543 + dependencies: 1544 + '@types/estree': 1.0.8 1545 + estree-walker: 2.0.2 1546 + picomatch: 4.0.4 1547 + optionalDependencies: 1548 + rollup: 4.60.2 1549 + 1550 + '@rollup/rollup-android-arm-eabi@4.60.2': 1551 + optional: true 1552 + 1553 + '@rollup/rollup-android-arm64@4.60.2': 1554 + optional: true 1555 + 1556 + '@rollup/rollup-darwin-arm64@4.60.2': 1557 + optional: true 1558 + 1559 + '@rollup/rollup-darwin-x64@4.60.2': 1560 + optional: true 1561 + 1562 + '@rollup/rollup-freebsd-arm64@4.60.2': 1563 + optional: true 1564 + 1565 + '@rollup/rollup-freebsd-x64@4.60.2': 1566 + optional: true 1567 + 1568 + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': 1569 + optional: true 1570 + 1571 + '@rollup/rollup-linux-arm-musleabihf@4.60.2': 1572 + optional: true 1573 + 1574 + '@rollup/rollup-linux-arm64-gnu@4.60.2': 1575 + optional: true 1576 + 1577 + '@rollup/rollup-linux-arm64-musl@4.60.2': 1578 + optional: true 1579 + 1580 + '@rollup/rollup-linux-loong64-gnu@4.60.2': 1581 + optional: true 1582 + 1583 + '@rollup/rollup-linux-loong64-musl@4.60.2': 1584 + optional: true 1585 + 1586 + '@rollup/rollup-linux-ppc64-gnu@4.60.2': 1587 + optional: true 1588 + 1589 + '@rollup/rollup-linux-ppc64-musl@4.60.2': 1590 + optional: true 1591 + 1592 + '@rollup/rollup-linux-riscv64-gnu@4.60.2': 1593 + optional: true 1594 + 1595 + '@rollup/rollup-linux-riscv64-musl@4.60.2': 1596 + optional: true 1597 + 1598 + '@rollup/rollup-linux-s390x-gnu@4.60.2': 1599 + optional: true 1600 + 1601 + '@rollup/rollup-linux-x64-gnu@4.60.2': 1602 + optional: true 1603 + 1604 + '@rollup/rollup-linux-x64-musl@4.60.2': 1605 + optional: true 1606 + 1607 + '@rollup/rollup-openbsd-x64@4.60.2': 1608 + optional: true 1609 + 1610 + '@rollup/rollup-openharmony-arm64@4.60.2': 1611 + optional: true 1612 + 1613 + '@rollup/rollup-win32-arm64-msvc@4.60.2': 1614 + optional: true 1615 + 1616 + '@rollup/rollup-win32-ia32-msvc@4.60.2': 1617 + optional: true 1618 + 1619 + '@rollup/rollup-win32-x64-gnu@4.60.2': 1620 + optional: true 1621 + 1622 + '@rollup/rollup-win32-x64-msvc@4.60.2': 1623 + optional: true 1624 + 1625 + '@types/better-sqlite3@7.6.13': 1626 + dependencies: 1627 + '@types/node': 22.19.17 1628 + 1629 + '@types/estree@1.0.8': {} 1630 + 1631 + '@types/node@22.19.17': 1632 + dependencies: 1633 + undici-types: 6.21.0 1634 + 1635 + ansi-regex@5.0.1: {} 1636 + 1637 + ansi-styles@4.3.0: 1638 + dependencies: 1639 + color-convert: 2.0.1 1640 + 1641 + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0): 1642 + dependencies: 1643 + '@babel/core': 7.29.0 1644 + 1645 + base64-js@1.5.1: {} 1646 + 1647 + baseline-browser-mapping@2.10.20: {} 1648 + 1649 + better-sqlite3@11.10.0: 1650 + dependencies: 1651 + bindings: 1.5.0 1652 + prebuild-install: 7.1.3 1653 + 1654 + bindings@1.5.0: 1655 + dependencies: 1656 + file-uri-to-path: 1.0.0 1657 + 1658 + bl@4.1.0: 1659 + dependencies: 1660 + buffer: 5.7.1 1661 + inherits: 2.0.4 1662 + readable-stream: 3.6.2 1663 + 1664 + boolbase@1.0.0: {} 1665 + 1666 + browserslist@4.28.2: 1667 + dependencies: 1668 + baseline-browser-mapping: 2.10.20 1669 + caniuse-lite: 1.0.30001788 1670 + electron-to-chromium: 1.5.340 1671 + node-releases: 2.0.37 1672 + update-browserslist-db: 1.2.3(browserslist@4.28.2) 1673 + 1674 + buffer@5.7.1: 1675 + dependencies: 1676 + base64-js: 1.5.1 1677 + ieee754: 1.2.1 1678 + 1679 + caniuse-lite@1.0.30001788: {} 1680 + 1681 + chalk@4.1.2: 1682 + dependencies: 1683 + ansi-styles: 4.3.0 1684 + supports-color: 7.2.0 1685 + 1686 + chownr@1.1.4: {} 1687 + 1688 + cliui@8.0.1: 1689 + dependencies: 1690 + string-width: 4.2.3 1691 + strip-ansi: 6.0.1 1692 + wrap-ansi: 7.0.0 1693 + 1694 + color-convert@2.0.1: 1695 + dependencies: 1696 + color-name: 1.1.4 1697 + 1698 + color-name@1.1.4: {} 1699 + 1700 + concurrently@9.2.1: 1701 + dependencies: 1702 + chalk: 4.1.2 1703 + rxjs: 7.8.2 1704 + shell-quote: 1.8.3 1705 + supports-color: 8.1.1 1706 + tree-kill: 1.2.2 1707 + yargs: 17.7.2 1708 + 1709 + convert-source-map@2.0.0: {} 1710 + 1711 + css-select@5.2.2: 1712 + dependencies: 1713 + boolbase: 1.0.0 1714 + css-what: 6.2.2 1715 + domhandler: 5.0.3 1716 + domutils: 3.2.2 1717 + nth-check: 2.1.1 1718 + 1719 + css-what@6.2.2: {} 1720 + 1721 + cssom@0.5.0: {} 1722 + 1723 + debug@4.4.3: 1724 + dependencies: 1725 + ms: 2.1.3 1726 + 1727 + decompress-response@6.0.0: 1728 + dependencies: 1729 + mimic-response: 3.1.0 1730 + 1731 + deep-extend@0.6.0: {} 1732 + 1733 + detect-libc@2.1.2: {} 1734 + 1735 + dom-serializer@2.0.0: 1736 + dependencies: 1737 + domelementtype: 2.3.0 1738 + domhandler: 5.0.3 1739 + entities: 4.5.0 1740 + 1741 + domelementtype@2.3.0: {} 1742 + 1743 + domhandler@5.0.3: 1744 + dependencies: 1745 + domelementtype: 2.3.0 1746 + 1747 + domutils@3.2.2: 1748 + dependencies: 1749 + dom-serializer: 2.0.0 1750 + domelementtype: 2.3.0 1751 + domhandler: 5.0.3 1752 + 1753 + dotenv@17.4.2: {} 1754 + 1755 + electron-to-chromium@1.5.340: {} 1756 + 1757 + emoji-regex@8.0.0: {} 1758 + 1759 + end-of-stream@1.4.5: 1760 + dependencies: 1761 + once: 1.4.0 1762 + 1763 + entities@4.5.0: {} 1764 + 1765 + entities@7.0.1: {} 1766 + 1767 + esbuild@0.25.12: 1768 + optionalDependencies: 1769 + '@esbuild/aix-ppc64': 0.25.12 1770 + '@esbuild/android-arm': 0.25.12 1771 + '@esbuild/android-arm64': 0.25.12 1772 + '@esbuild/android-x64': 0.25.12 1773 + '@esbuild/darwin-arm64': 0.25.12 1774 + '@esbuild/darwin-x64': 0.25.12 1775 + '@esbuild/freebsd-arm64': 0.25.12 1776 + '@esbuild/freebsd-x64': 0.25.12 1777 + '@esbuild/linux-arm': 0.25.12 1778 + '@esbuild/linux-arm64': 0.25.12 1779 + '@esbuild/linux-ia32': 0.25.12 1780 + '@esbuild/linux-loong64': 0.25.12 1781 + '@esbuild/linux-mips64el': 0.25.12 1782 + '@esbuild/linux-ppc64': 0.25.12 1783 + '@esbuild/linux-riscv64': 0.25.12 1784 + '@esbuild/linux-s390x': 0.25.12 1785 + '@esbuild/linux-x64': 0.25.12 1786 + '@esbuild/netbsd-arm64': 0.25.12 1787 + '@esbuild/netbsd-x64': 0.25.12 1788 + '@esbuild/openbsd-arm64': 0.25.12 1789 + '@esbuild/openbsd-x64': 0.25.12 1790 + '@esbuild/openharmony-arm64': 0.25.12 1791 + '@esbuild/sunos-x64': 0.25.12 1792 + '@esbuild/win32-arm64': 0.25.12 1793 + '@esbuild/win32-ia32': 0.25.12 1794 + '@esbuild/win32-x64': 0.25.12 1795 + 1796 + esbuild@0.27.7: 1797 + optionalDependencies: 1798 + '@esbuild/aix-ppc64': 0.27.7 1799 + '@esbuild/android-arm': 0.27.7 1800 + '@esbuild/android-arm64': 0.27.7 1801 + '@esbuild/android-x64': 0.27.7 1802 + '@esbuild/darwin-arm64': 0.27.7 1803 + '@esbuild/darwin-x64': 0.27.7 1804 + '@esbuild/freebsd-arm64': 0.27.7 1805 + '@esbuild/freebsd-x64': 0.27.7 1806 + '@esbuild/linux-arm': 0.27.7 1807 + '@esbuild/linux-arm64': 0.27.7 1808 + '@esbuild/linux-ia32': 0.27.7 1809 + '@esbuild/linux-loong64': 0.27.7 1810 + '@esbuild/linux-mips64el': 0.27.7 1811 + '@esbuild/linux-ppc64': 0.27.7 1812 + '@esbuild/linux-riscv64': 0.27.7 1813 + '@esbuild/linux-s390x': 0.27.7 1814 + '@esbuild/linux-x64': 0.27.7 1815 + '@esbuild/netbsd-arm64': 0.27.7 1816 + '@esbuild/netbsd-x64': 0.27.7 1817 + '@esbuild/openbsd-arm64': 0.27.7 1818 + '@esbuild/openbsd-x64': 0.27.7 1819 + '@esbuild/openharmony-arm64': 0.27.7 1820 + '@esbuild/sunos-x64': 0.27.7 1821 + '@esbuild/win32-arm64': 0.27.7 1822 + '@esbuild/win32-ia32': 0.27.7 1823 + '@esbuild/win32-x64': 0.27.7 1824 + 1825 + escalade@3.2.0: {} 1826 + 1827 + estree-walker@2.0.2: {} 1828 + 1829 + expand-template@2.0.3: {} 1830 + 1831 + fdir@6.5.0(picomatch@4.0.4): 1832 + optionalDependencies: 1833 + picomatch: 4.0.4 1834 + 1835 + file-uri-to-path@1.0.0: {} 1836 + 1837 + fs-constants@1.0.0: {} 1838 + 1839 + fsevents@2.3.3: 1840 + optional: true 1841 + 1842 + gensync@1.0.0-beta.2: {} 1843 + 1844 + get-caller-file@2.0.5: {} 1845 + 1846 + get-tsconfig@4.14.0: 1847 + dependencies: 1848 + resolve-pkg-maps: 1.0.0 1849 + 1850 + github-from-package@0.0.0: {} 1851 + 1852 + has-flag@4.0.0: {} 1853 + 1854 + he@1.2.0: {} 1855 + 1856 + hono@4.12.14: {} 1857 + 1858 + html-escaper@3.0.3: {} 1859 + 1860 + htmlparser2@10.1.0: 1861 + dependencies: 1862 + domelementtype: 2.3.0 1863 + domhandler: 5.0.3 1864 + domutils: 3.2.2 1865 + entities: 7.0.1 1866 + 1867 + ieee754@1.2.1: {} 1868 + 1869 + inherits@2.0.4: {} 1870 + 1871 + ini@1.3.8: {} 1872 + 1873 + is-fullwidth-code-point@3.0.0: {} 1874 + 1875 + js-tokens@4.0.0: {} 1876 + 1877 + jsesc@3.1.0: {} 1878 + 1879 + json5@2.2.3: {} 1880 + 1881 + kolorist@1.8.0: {} 1882 + 1883 + linkedom@0.18.12: 1884 + dependencies: 1885 + css-select: 5.2.2 1886 + cssom: 0.5.0 1887 + html-escaper: 3.0.3 1888 + htmlparser2: 10.1.0 1889 + uhyphen: 0.2.0 1890 + 1891 + lru-cache@5.1.1: 1892 + dependencies: 1893 + yallist: 3.1.1 1894 + 1895 + magic-string@0.30.21: 1896 + dependencies: 1897 + '@jridgewell/sourcemap-codec': 1.5.5 1898 + 1899 + mimic-response@3.1.0: {} 1900 + 1901 + minimist@1.2.8: {} 1902 + 1903 + mkdirp-classic@0.5.3: {} 1904 + 1905 + ms@2.1.3: {} 1906 + 1907 + nanoid@3.3.11: {} 1908 + 1909 + napi-build-utils@2.0.0: {} 1910 + 1911 + node-abi@3.89.0: 1912 + dependencies: 1913 + semver: 7.7.4 1914 + 1915 + node-html-parser@6.1.13: 1916 + dependencies: 1917 + css-select: 5.2.2 1918 + he: 1.2.0 1919 + 1920 + node-releases@2.0.37: {} 1921 + 1922 + nth-check@2.1.1: 1923 + dependencies: 1924 + boolbase: 1.0.0 1925 + 1926 + once@1.4.0: 1927 + dependencies: 1928 + wrappy: 1.0.2 1929 + 1930 + picocolors@1.1.1: {} 1931 + 1932 + picomatch@2.3.2: {} 1933 + 1934 + picomatch@4.0.4: {} 1935 + 1936 + postcss@8.5.10: 1937 + dependencies: 1938 + nanoid: 3.3.11 1939 + picocolors: 1.1.1 1940 + source-map-js: 1.2.1 1941 + 1942 + preact@10.29.1: {} 1943 + 1944 + prebuild-install@7.1.3: 1945 + dependencies: 1946 + detect-libc: 2.1.2 1947 + expand-template: 2.0.3 1948 + github-from-package: 0.0.0 1949 + minimist: 1.2.8 1950 + mkdirp-classic: 0.5.3 1951 + napi-build-utils: 2.0.0 1952 + node-abi: 3.89.0 1953 + pump: 3.0.4 1954 + rc: 1.2.8 1955 + simple-get: 4.0.1 1956 + tar-fs: 2.1.4 1957 + tunnel-agent: 0.6.0 1958 + 1959 + pump@3.0.4: 1960 + dependencies: 1961 + end-of-stream: 1.4.5 1962 + once: 1.4.0 1963 + 1964 + rc@1.2.8: 1965 + dependencies: 1966 + deep-extend: 0.6.0 1967 + ini: 1.3.8 1968 + minimist: 1.2.8 1969 + strip-json-comments: 2.0.1 1970 + 1971 + readable-stream@3.6.2: 1972 + dependencies: 1973 + inherits: 2.0.4 1974 + string_decoder: 1.3.0 1975 + util-deprecate: 1.0.2 1976 + 1977 + require-directory@2.1.1: {} 1978 + 1979 + resolve-pkg-maps@1.0.0: {} 1980 + 1981 + rollup@4.60.2: 1982 + dependencies: 1983 + '@types/estree': 1.0.8 1984 + optionalDependencies: 1985 + '@rollup/rollup-android-arm-eabi': 4.60.2 1986 + '@rollup/rollup-android-arm64': 4.60.2 1987 + '@rollup/rollup-darwin-arm64': 4.60.2 1988 + '@rollup/rollup-darwin-x64': 4.60.2 1989 + '@rollup/rollup-freebsd-arm64': 4.60.2 1990 + '@rollup/rollup-freebsd-x64': 4.60.2 1991 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 1992 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 1993 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 1994 + '@rollup/rollup-linux-arm64-musl': 4.60.2 1995 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 1996 + '@rollup/rollup-linux-loong64-musl': 4.60.2 1997 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 1998 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 1999 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 2000 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 2001 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 2002 + '@rollup/rollup-linux-x64-gnu': 4.60.2 2003 + '@rollup/rollup-linux-x64-musl': 4.60.2 2004 + '@rollup/rollup-openbsd-x64': 4.60.2 2005 + '@rollup/rollup-openharmony-arm64': 4.60.2 2006 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 2007 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 2008 + '@rollup/rollup-win32-x64-gnu': 4.60.2 2009 + '@rollup/rollup-win32-x64-msvc': 4.60.2 2010 + fsevents: 2.3.3 2011 + 2012 + rxjs@7.8.2: 2013 + dependencies: 2014 + tslib: 2.8.1 2015 + 2016 + safe-buffer@5.2.1: {} 2017 + 2018 + semver@6.3.1: {} 2019 + 2020 + semver@7.7.4: {} 2021 + 2022 + shell-quote@1.8.3: {} 2023 + 2024 + simple-code-frame@1.3.0: 2025 + dependencies: 2026 + kolorist: 1.8.0 2027 + 2028 + simple-concat@1.0.1: {} 2029 + 2030 + simple-get@4.0.1: 2031 + dependencies: 2032 + decompress-response: 6.0.0 2033 + once: 1.4.0 2034 + simple-concat: 1.0.1 2035 + 2036 + source-map-js@1.2.1: {} 2037 + 2038 + source-map@0.7.6: {} 2039 + 2040 + stack-trace@1.0.0-pre2: {} 2041 + 2042 + string-width@4.2.3: 2043 + dependencies: 2044 + emoji-regex: 8.0.0 2045 + is-fullwidth-code-point: 3.0.0 2046 + strip-ansi: 6.0.1 2047 + 2048 + string_decoder@1.3.0: 2049 + dependencies: 2050 + safe-buffer: 5.2.1 2051 + 2052 + strip-ansi@6.0.1: 2053 + dependencies: 2054 + ansi-regex: 5.0.1 2055 + 2056 + strip-json-comments@2.0.1: {} 2057 + 2058 + supports-color@7.2.0: 2059 + dependencies: 2060 + has-flag: 4.0.0 2061 + 2062 + supports-color@8.1.1: 2063 + dependencies: 2064 + has-flag: 4.0.0 2065 + 2066 + tar-fs@2.1.4: 2067 + dependencies: 2068 + chownr: 1.1.4 2069 + mkdirp-classic: 0.5.3 2070 + pump: 3.0.4 2071 + tar-stream: 2.2.0 2072 + 2073 + tar-stream@2.2.0: 2074 + dependencies: 2075 + bl: 4.1.0 2076 + end-of-stream: 1.4.5 2077 + fs-constants: 1.0.0 2078 + inherits: 2.0.4 2079 + readable-stream: 3.6.2 2080 + 2081 + tinyglobby@0.2.16: 2082 + dependencies: 2083 + fdir: 6.5.0(picomatch@4.0.4) 2084 + picomatch: 4.0.4 2085 + 2086 + tree-kill@1.2.2: {} 2087 + 2088 + tslib@2.8.1: {} 2089 + 2090 + tsx@4.21.0: 2091 + dependencies: 2092 + esbuild: 0.27.7 2093 + get-tsconfig: 4.14.0 2094 + optionalDependencies: 2095 + fsevents: 2.3.3 2096 + 2097 + tunnel-agent@0.6.0: 2098 + dependencies: 2099 + safe-buffer: 5.2.1 2100 + 2101 + typescript@5.9.3: {} 2102 + 2103 + uhyphen@0.2.0: {} 2104 + 2105 + undici-types@6.21.0: {} 2106 + 2107 + update-browserslist-db@1.2.3(browserslist@4.28.2): 2108 + dependencies: 2109 + browserslist: 4.28.2 2110 + escalade: 3.2.0 2111 + picocolors: 1.1.1 2112 + 2113 + util-deprecate@1.0.2: {} 2114 + 2115 + vite-prerender-plugin@0.5.13(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)): 2116 + dependencies: 2117 + kolorist: 1.8.0 2118 + magic-string: 0.30.21 2119 + node-html-parser: 6.1.13 2120 + simple-code-frame: 1.3.0 2121 + source-map: 0.7.6 2122 + stack-trace: 1.0.0-pre2 2123 + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0) 2124 + 2125 + vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0): 2126 + dependencies: 2127 + esbuild: 0.25.12 2128 + fdir: 6.5.0(picomatch@4.0.4) 2129 + picomatch: 4.0.4 2130 + postcss: 8.5.10 2131 + rollup: 4.60.2 2132 + tinyglobby: 0.2.16 2133 + optionalDependencies: 2134 + '@types/node': 22.19.17 2135 + fsevents: 2.3.3 2136 + tsx: 4.21.0 2137 + 2138 + wrap-ansi@7.0.0: 2139 + dependencies: 2140 + ansi-styles: 4.3.0 2141 + string-width: 4.2.3 2142 + strip-ansi: 6.0.1 2143 + 2144 + wrappy@1.0.2: {} 2145 + 2146 + y18n@5.0.8: {} 2147 + 2148 + yallist@3.1.1: {} 2149 + 2150 + yargs-parser@21.1.1: {} 2151 + 2152 + yargs@17.7.2: 2153 + dependencies: 2154 + cliui: 8.0.1 2155 + escalade: 3.2.0 2156 + get-caller-file: 2.0.5 2157 + require-directory: 2.1.1 2158 + string-width: 4.2.3 2159 + y18n: 5.0.8 2160 + yargs-parser: 21.1.1 2161 + 2162 + zimmerframe@1.1.4: {}
+84
src/server/db.ts
··· 1 + import Database from "better-sqlite3"; 2 + import type { SavedItem } from "../shared/types.js"; 3 + 4 + export class Store { 5 + private db: Database.Database; 6 + 7 + constructor(path: string) { 8 + this.db = new Database(path); 9 + this.db.pragma("journal_mode = WAL"); 10 + this.db.exec(` 11 + CREATE TABLE IF NOT EXISTS saves ( 12 + id INTEGER PRIMARY KEY AUTOINCREMENT, 13 + url TEXT UNIQUE NOT NULL, 14 + title TEXT NOT NULL DEFAULT '', 15 + body TEXT NOT NULL DEFAULT '', 16 + fetched_at INTEGER NOT NULL, 17 + read INTEGER NOT NULL DEFAULT 0 18 + ); 19 + CREATE INDEX IF NOT EXISTS idx_saves_fetched ON saves(fetched_at DESC); 20 + `); 21 + } 22 + 23 + insertSave(url: string, title: string, body: string, ts: number): number { 24 + const info = this.db 25 + .prepare( 26 + "INSERT INTO saves (url, title, body, fetched_at) VALUES (?, ?, ?, ?)", 27 + ) 28 + .run(url, title, body, ts); 29 + return Number(info.lastInsertRowid); 30 + } 31 + 32 + saveExists(url: string): boolean { 33 + const row = this.db 34 + .prepare("SELECT 1 AS x FROM saves WHERE url = ?") 35 + .get(url); 36 + return row !== undefined; 37 + } 38 + 39 + getSave(id: number): SavedItem | null { 40 + const row = this.db 41 + .prepare( 42 + "SELECT id, url, title, body, fetched_at AS fetchedAt, read FROM saves WHERE id = ?", 43 + ) 44 + .get(id) as 45 + | { 46 + id: number; 47 + url: string; 48 + title: string; 49 + body: string; 50 + fetchedAt: number; 51 + read: number; 52 + } 53 + | undefined; 54 + if (!row) return null; 55 + return { ...row, read: row.read === 1 }; 56 + } 57 + 58 + listSaves(unreadOnly: boolean, limit: number): SavedItem[] { 59 + const where = unreadOnly ? "WHERE read = 0" : ""; 60 + const rows = this.db 61 + .prepare( 62 + `SELECT id, url, title, body, fetched_at AS fetchedAt, read FROM saves ${where} ORDER BY fetched_at DESC LIMIT ?`, 63 + ) 64 + .all(limit) as Array<{ 65 + id: number; 66 + url: string; 67 + title: string; 68 + body: string; 69 + fetchedAt: number; 70 + read: number; 71 + }>; 72 + return rows.map((r) => ({ ...r, read: r.read === 1 })); 73 + } 74 + 75 + markSaveRead(id: number, read: boolean): void { 76 + this.db 77 + .prepare("UPDATE saves SET read = ? WHERE id = ?") 78 + .run(read ? 1 : 0, id); 79 + } 80 + 81 + deleteSave(id: number): void { 82 + this.db.prepare("DELETE FROM saves WHERE id = ?").run(id); 83 + } 84 + }
+55
src/server/index.ts
··· 1 + import { readFileSync, existsSync } from "node:fs"; 2 + import { resolve } from "node:path"; 3 + import { serve } from "@hono/node-server"; 4 + import { serveStatic } from "@hono/node-server/serve-static"; 5 + import { Hono } from "hono"; 6 + import { logger } from "hono/logger"; 7 + import "dotenv/config"; 8 + 9 + import { Store } from "./db.js"; 10 + import { MinifluxClient } from "./miniflux.js"; 11 + import { apiRoutes } from "./routes-api.js"; 12 + import { deviceRoutes } from "./routes-device.js"; 13 + 14 + const port = Number(process.env.NIGHTSHADE_PORT ?? 8787); 15 + const dbPath = process.env.NIGHTSHADE_DB ?? "./nightshade.sqlite3"; 16 + const mfUrl = process.env.MINIFLUX_URL; 17 + const mfToken = 18 + process.env.MINIFLUX_TOKEN ?? 19 + (process.env.MINIFLUX_TOKEN_FILE 20 + ? readFileSync(process.env.MINIFLUX_TOKEN_FILE, "utf8").trim() 21 + : undefined); 22 + 23 + if (!mfUrl || !mfToken) { 24 + console.error( 25 + "MINIFLUX_URL and (MINIFLUX_TOKEN or MINIFLUX_TOKEN_FILE) must be set", 26 + ); 27 + process.exit(1); 28 + } 29 + 30 + const store = new Store(dbPath); 31 + const mf = new MinifluxClient(mfUrl, mfToken); 32 + 33 + const app = new Hono(); 34 + app.use(logger()); 35 + 36 + app.route("/api", apiRoutes(store, mf)); 37 + app.route("/device", deviceRoutes(store, mf)); 38 + 39 + const publicDir = resolve("./dist/public"); 40 + const indexPath = resolve(publicDir, "index.html"); 41 + const indexHtml = existsSync(indexPath) ? readFileSync(indexPath, "utf8") : ""; 42 + 43 + app.use("/*", serveStatic({ root: "./dist/public" })); 44 + 45 + // SPA fallback 46 + app.get("*", (c) => { 47 + if (!indexHtml) { 48 + return c.text("Frontend not built. Run `pnpm build:web`.\n", 200); 49 + } 50 + return c.html(indexHtml); 51 + }); 52 + 53 + serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => { 54 + console.log(`nightshade listening on http://${info.address}:${info.port}`); 55 + });
+84
src/server/miniflux.ts
··· 1 + import type { MinifluxEntry, MinifluxFeed } from "../shared/types.js"; 2 + 3 + export class MinifluxClient { 4 + constructor( 5 + private baseUrl: string, 6 + private token: string, 7 + ) { 8 + this.baseUrl = baseUrl.replace(/\/$/, ""); 9 + } 10 + 11 + private async req<T>(path: string, init: RequestInit = {}): Promise<T> { 12 + const res = await fetch(this.baseUrl + path, { 13 + ...init, 14 + headers: { 15 + "X-Auth-Token": this.token, 16 + "Content-Type": "application/json", 17 + ...(init.headers ?? {}), 18 + }, 19 + }); 20 + if (!res.ok) { 21 + const text = await res.text().catch(() => ""); 22 + throw new Error(`miniflux ${res.status}: ${text}`); 23 + } 24 + if (res.status === 204) return undefined as T; 25 + return res.json() as Promise<T>; 26 + } 27 + 28 + async listFeeds(): Promise<MinifluxFeed[]> { 29 + return this.req("/v1/feeds"); 30 + } 31 + 32 + async subscribe( 33 + feedUrl: string, 34 + categoryId = 1, 35 + ): Promise<{ feed_id: number }> { 36 + return this.req("/v1/feeds", { 37 + method: "POST", 38 + body: JSON.stringify({ feed_url: feedUrl, category_id: categoryId }), 39 + }); 40 + } 41 + 42 + async deleteFeed(id: number): Promise<void> { 43 + await this.req(`/v1/feeds/${id}`, { method: "DELETE" }); 44 + } 45 + 46 + async refreshFeed(id: number): Promise<void> { 47 + await this.req(`/v1/feeds/${id}/refresh`, { method: "PUT" }); 48 + } 49 + 50 + async refreshAll(): Promise<void> { 51 + await this.req("/v1/feeds/refresh", { method: "PUT" }); 52 + } 53 + 54 + async listEntries( 55 + params: { 56 + status?: "unread" | "read"; 57 + limit?: number; 58 + order?: "published_at" | "created_at" | "id"; 59 + direction?: "asc" | "desc"; 60 + } = {}, 61 + ): Promise<{ total: number; entries: MinifluxEntry[] }> { 62 + const q = new URLSearchParams(); 63 + if (params.status) q.set("status", params.status); 64 + q.set("limit", String(params.limit ?? 100)); 65 + q.set("order", params.order ?? "published_at"); 66 + q.set("direction", params.direction ?? "desc"); 67 + return this.req(`/v1/entries?${q}`); 68 + } 69 + 70 + async getEntry(id: number): Promise<MinifluxEntry> { 71 + return this.req(`/v1/entries/${id}`); 72 + } 73 + 74 + async markEntries(ids: number[], status: "read" | "unread"): Promise<void> { 75 + await this.req("/v1/entries", { 76 + method: "PUT", 77 + body: JSON.stringify({ entry_ids: ids, status }), 78 + }); 79 + } 80 + 81 + async getCategories(): Promise<Array<{ id: number; title: string }>> { 82 + return this.req("/v1/categories"); 83 + } 84 + }
+129
src/server/readability.ts
··· 1 + import { Readability } from "@mozilla/readability"; 2 + import { parseHTML } from "linkedom"; 3 + 4 + const BLOCK = new Set([ 5 + "P", 6 + "DIV", 7 + "SECTION", 8 + "ARTICLE", 9 + "HEADER", 10 + "FOOTER", 11 + "ASIDE", 12 + "MAIN", 13 + "H1", 14 + "H2", 15 + "H3", 16 + "H4", 17 + "H5", 18 + "H6", 19 + "UL", 20 + "OL", 21 + "LI", 22 + "DL", 23 + "DT", 24 + "DD", 25 + "BLOCKQUOTE", 26 + "PRE", 27 + "FIGURE", 28 + "FIGCAPTION", 29 + "TABLE", 30 + "THEAD", 31 + "TBODY", 32 + "TFOOT", 33 + "TR", 34 + "TD", 35 + "TH", 36 + "HR", 37 + ]); 38 + 39 + const SKIP = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]); 40 + 41 + export async function fetchAndExtract( 42 + url: string, 43 + ): Promise<{ title: string; body: string }> { 44 + const res = await fetch(url, { 45 + headers: { 46 + "User-Agent": "Nightshade/1.0", 47 + Accept: "text/html,application/xhtml+xml", 48 + }, 49 + redirect: "follow", 50 + }); 51 + if (!res.ok) throw new Error(`http ${res.status}`); 52 + const html = await res.text(); 53 + return extract(html, url); 54 + } 55 + 56 + export function extract( 57 + html: string, 58 + url: string, 59 + ): { title: string; body: string } { 60 + const { document } = parseHTML(html); 61 + const base = document.createElement("base"); 62 + base.setAttribute("href", url); 63 + document.head?.appendChild(base); 64 + const reader = new Readability(document as unknown as Document); 65 + const article = reader.parse(); 66 + if (!article) throw new Error("readability produced no article"); 67 + const title = article.title ?? url; 68 + let body: string; 69 + if (article.content) { 70 + const { document: d2 } = parseHTML( 71 + `<!doctype html><html><body>${article.content}</body></html>`, 72 + ); 73 + body = htmlToText(d2.body); 74 + } else { 75 + body = (article.textContent ?? "").trim(); 76 + } 77 + return { title, body }; 78 + } 79 + 80 + export function htmlToText(root: unknown): string { 81 + const out: string[] = []; 82 + walk(root as Node | null, out); 83 + return out 84 + .join("") 85 + .replace(/[ \t]+\n/g, "\n") 86 + .replace(/\n{3,}/g, "\n\n") 87 + .replace(/[ \t]{2,}/g, " ") 88 + .trim(); 89 + } 90 + 91 + function walk(node: Node | null, out: string[]): void { 92 + if (!node) return; 93 + const type = node.nodeType; 94 + if (type === 3) { 95 + out.push(node.nodeValue ?? ""); 96 + return; 97 + } 98 + if (type !== 1) return; 99 + const el = node as Element; 100 + const tag = el.nodeName; 101 + if (SKIP.has(tag)) return; 102 + if (tag === "BR") { 103 + out.push("\n"); 104 + return; 105 + } 106 + const isBlock = BLOCK.has(tag); 107 + if (isBlock) ensureBreak(out); 108 + let child = el.firstChild; 109 + while (child) { 110 + walk(child, out); 111 + if (!isBlock && child.nodeType === 1) ensureSpace(out); 112 + child = child.nextSibling; 113 + } 114 + if (isBlock) ensureBreak(out); 115 + } 116 + 117 + function ensureBreak(out: string[]): void { 118 + const tail = out.length ? out[out.length - 1]! : ""; 119 + if (!tail.endsWith("\n\n")) { 120 + out.push(tail.endsWith("\n") ? "\n" : "\n\n"); 121 + } 122 + } 123 + 124 + function ensureSpace(out: string[]): void { 125 + const tail = out.length ? out[out.length - 1]! : ""; 126 + if (!tail) return; 127 + const last = tail[tail.length - 1]; 128 + if (last && !/\s/.test(last)) out.push(" "); 129 + }
+83
src/server/reader-format.ts
··· 1 + import type { ItemSource, UnifiedItem } from "../shared/types.js"; 2 + 3 + export const LINE_WIDTH = 60; 4 + export const PAGE_BODY_LINES = 24; 5 + 6 + export function renderList(items: UnifiedItem[]): string { 7 + const lines: string[] = [`COUNT:${items.length}`, "==="]; 8 + for (const it of items) { 9 + lines.push(`ID:${it.id}`); 10 + lines.push(`S:${it.source}`); 11 + lines.push(`R:${it.read ? 1 : 0}`); 12 + lines.push(`D:${it.fetchedAt}`); 13 + lines.push(`T:${singleLine(it.title)}`); 14 + lines.push("==="); 15 + } 16 + return lines.join("\n") + "\n"; 17 + } 18 + 19 + export function renderItem( 20 + id: string, 21 + source: ItemSource, 22 + title: string, 23 + url: string, 24 + fetchedAt: number, 25 + body: string, 26 + page: number, 27 + ): string { 28 + const wrapped = wrapLines(body, LINE_WIDTH); 29 + const totalPages = Math.max(1, Math.ceil(wrapped.length / PAGE_BODY_LINES)); 30 + const p = clamp(page, 1, totalPages); 31 + const start = (p - 1) * PAGE_BODY_LINES; 32 + const pageLines = wrapped.slice(start, start + PAGE_BODY_LINES); 33 + return [ 34 + `ID:${id}`, 35 + `S:${source}`, 36 + `T:${singleLine(title)}`, 37 + `U:${singleLine(url)}`, 38 + `D:${fetchedAt}`, 39 + `P:${p}/${totalPages}`, 40 + "---", 41 + ...pageLines, 42 + "", 43 + ].join("\n"); 44 + } 45 + 46 + function singleLine(s: string): string { 47 + return s.replace(/[\r\n]+/g, " ").trim(); 48 + } 49 + 50 + function clamp(n: number, lo: number, hi: number): number { 51 + return Math.max(lo, Math.min(hi, n)); 52 + } 53 + 54 + export function wrapLines(text: string, width: number): string[] { 55 + const paragraphs = text.split("\n"); 56 + const out: string[] = []; 57 + for (const p of paragraphs) { 58 + const trimmed = p.trim(); 59 + if (!trimmed) { 60 + out.push(""); 61 + continue; 62 + } 63 + out.push(...wrapParagraph(trimmed, width)); 64 + } 65 + return out; 66 + } 67 + 68 + function wrapParagraph(p: string, width: number): string[] { 69 + const words = p.split(/\s+/); 70 + const out: string[] = []; 71 + let cur = ""; 72 + for (const w of words) { 73 + const cand = cur ? cur + " " + w : w; 74 + if (cand.length > width && cur) { 75 + out.push(cur); 76 + cur = w; 77 + } else { 78 + cur = cand; 79 + } 80 + } 81 + if (cur) out.push(cur); 82 + return out; 83 + }
+82
src/server/routes-api.ts
··· 1 + import { Hono } from "hono"; 2 + import type { Store } from "./db.js"; 3 + import type { MinifluxClient } from "./miniflux.js"; 4 + import { fetchAndExtract } from "./readability.js"; 5 + 6 + export function apiRoutes(store: Store, mf: MinifluxClient) { 7 + const app = new Hono(); 8 + 9 + // --- Saved URLs --- 10 + app.get("/saves", (c) => { 11 + const unread = c.req.query("all") === undefined; 12 + const limit = Number(c.req.query("limit") ?? 100); 13 + return c.json(store.listSaves(unread, limit)); 14 + }); 15 + 16 + app.post("/saves", async (c) => { 17 + const { url } = await c.req.json<{ url: string }>(); 18 + if (!url) return c.json({ error: "missing url" }, 400); 19 + if (store.saveExists(url)) return c.json({ error: "already saved" }, 409); 20 + try { 21 + const { title, body } = await fetchAndExtract(url); 22 + const id = store.insertSave( 23 + url, 24 + title, 25 + body, 26 + Math.floor(Date.now() / 1000), 27 + ); 28 + return c.json({ id, title }); 29 + } catch (e) { 30 + return c.json({ error: String((e as Error).message ?? e) }, 502); 31 + } 32 + }); 33 + 34 + app.delete("/saves/:id", (c) => { 35 + store.deleteSave(Number(c.req.param("id"))); 36 + return c.body(null, 204); 37 + }); 38 + 39 + app.post("/saves/:id/read", (c) => { 40 + store.markSaveRead(Number(c.req.param("id")), true); 41 + return c.body(null, 204); 42 + }); 43 + 44 + app.post("/saves/:id/unread", (c) => { 45 + store.markSaveRead(Number(c.req.param("id")), false); 46 + return c.body(null, 204); 47 + }); 48 + 49 + // --- Miniflux passthrough --- 50 + app.get("/feeds", async (c) => { 51 + const feeds = await mf.listFeeds(); 52 + return c.json(feeds); 53 + }); 54 + 55 + app.post("/feeds", async (c) => { 56 + const { url, category_id } = await c.req.json<{ 57 + url: string; 58 + category_id?: number; 59 + }>(); 60 + if (!url) return c.json({ error: "missing url" }, 400); 61 + try { 62 + const result = await mf.subscribe(url, category_id ?? 1); 63 + return c.json(result); 64 + } catch (e) { 65 + return c.json({ error: String((e as Error).message ?? e) }, 502); 66 + } 67 + }); 68 + 69 + app.delete("/feeds/:id", async (c) => { 70 + await mf.deleteFeed(Number(c.req.param("id"))); 71 + return c.body(null, 204); 72 + }); 73 + 74 + app.post("/feeds/refresh", async (c) => { 75 + await mf.refreshAll(); 76 + return c.body(null, 204); 77 + }); 78 + 79 + app.get("/categories", async (c) => c.json(await mf.getCategories())); 80 + 81 + return app; 82 + }
+113
src/server/routes-device.ts
··· 1 + import { Hono } from "hono"; 2 + import type { Store } from "./db.js"; 3 + import type { MinifluxClient } from "./miniflux.js"; 4 + import { htmlToText } from "./readability.js"; 5 + import { parseHTML } from "linkedom"; 6 + import { renderItem, renderList } from "./reader-format.js"; 7 + import type { UnifiedItem } from "../shared/types.js"; 8 + 9 + export function deviceRoutes(store: Store, mf: MinifluxClient) { 10 + const app = new Hono(); 11 + 12 + app.get("/list", async (c) => { 13 + const all = c.req.query("all") !== undefined; 14 + const limit = Number(c.req.query("limit") ?? 50); 15 + 16 + const [entriesRes, saves] = await Promise.all([ 17 + mf.listEntries({ 18 + status: all ? undefined : "unread", 19 + limit, 20 + order: "published_at", 21 + direction: "desc", 22 + }), 23 + Promise.resolve(store.listSaves(!all, limit)), 24 + ]); 25 + 26 + const items: UnifiedItem[] = []; 27 + for (const e of entriesRes.entries) { 28 + items.push({ 29 + source: "rss", 30 + id: String(e.id), 31 + url: e.url, 32 + title: e.title, 33 + fetchedAt: Math.floor(new Date(e.published_at).getTime() / 1000), 34 + read: e.status === "read", 35 + }); 36 + } 37 + for (const s of saves) { 38 + items.push({ 39 + source: "save", 40 + id: `s${s.id}`, 41 + url: s.url, 42 + title: s.title, 43 + fetchedAt: s.fetchedAt, 44 + read: s.read, 45 + }); 46 + } 47 + items.sort((a, b) => b.fetchedAt - a.fetchedAt); 48 + const sliced = items.slice(0, limit); 49 + 50 + return c.text(renderList(sliced), 200, { 51 + "Content-Type": "text/plain; charset=utf-8", 52 + }); 53 + }); 54 + 55 + app.get("/item/:id", async (c) => { 56 + const rawId = c.req.param("id"); 57 + const page = Number(c.req.query("page") ?? 1); 58 + try { 59 + if (rawId.startsWith("s")) { 60 + const save = store.getSave(Number(rawId.slice(1))); 61 + if (!save) return c.text("not found\n", 404); 62 + return c.text( 63 + renderItem( 64 + rawId, 65 + "save", 66 + save.title, 67 + save.url, 68 + save.fetchedAt, 69 + save.body, 70 + page, 71 + ), 72 + 200, 73 + { "Content-Type": "text/plain; charset=utf-8" }, 74 + ); 75 + } 76 + const entry = await mf.getEntry(Number(rawId)); 77 + const body = entry.content 78 + ? extractFromStoredHtml(entry.content, entry.url) 79 + : ""; 80 + const ts = Math.floor(new Date(entry.published_at).getTime() / 1000); 81 + return c.text( 82 + renderItem(rawId, "rss", entry.title, entry.url, ts, body, page), 83 + 200, 84 + { "Content-Type": "text/plain; charset=utf-8" }, 85 + ); 86 + } catch (e) { 87 + return c.text(`error: ${(e as Error).message}\n`, 502); 88 + } 89 + }); 90 + 91 + app.post("/item/:id/read", async (c) => { 92 + const rawId = c.req.param("id"); 93 + if (rawId.startsWith("s")) { 94 + store.markSaveRead(Number(rawId.slice(1)), true); 95 + } else { 96 + await mf.markEntries([Number(rawId)], "read"); 97 + } 98 + return c.text("ok\n"); 99 + }); 100 + 101 + return app; 102 + } 103 + 104 + function extractFromStoredHtml(html: string, _url: string): string { 105 + try { 106 + const { document } = parseHTML( 107 + `<!doctype html><html><body>${html}</body></html>`, 108 + ); 109 + return htmlToText(document.body); 110 + } catch { 111 + return html; 112 + } 113 + }
+41
src/shared/types.ts
··· 1 + export type ItemSource = "rss" | "save"; 2 + 3 + export type UnifiedItem = { 4 + source: ItemSource; 5 + // For rss: miniflux entry id. For save: local saves table id, prefixed with "s". 6 + id: string; 7 + url: string; 8 + title: string; 9 + fetchedAt: number; 10 + read: boolean; 11 + }; 12 + 13 + export type SavedItem = { 14 + id: number; 15 + url: string; 16 + title: string; 17 + body: string; 18 + fetchedAt: number; 19 + read: boolean; 20 + }; 21 + 22 + export type MinifluxFeed = { 23 + id: number; 24 + user_id: number; 25 + feed_url: string; 26 + site_url: string; 27 + title: string; 28 + category: { id: number; title: string }; 29 + }; 30 + 31 + export type MinifluxEntry = { 32 + id: number; 33 + feed_id: number; 34 + status: "unread" | "read" | "removed"; 35 + url: string; 36 + title: string; 37 + content: string; 38 + published_at: string; 39 + starred: boolean; 40 + feed: { id: number; title: string; site_url: string }; 41 + };
+314
src/web/App.tsx
··· 1 + import { useEffect, useState } from "preact/hooks"; 2 + import { api } from "./api.js"; 3 + import type { MinifluxFeed, SavedItem } from "../shared/types.js"; 4 + 5 + type Tab = "saves" | "feeds"; 6 + 7 + export function App() { 8 + const [tab, setTab] = useState<Tab>("saves"); 9 + 10 + return ( 11 + <> 12 + <header> 13 + <h1>Nightshade</h1> 14 + <nav> 15 + <button 16 + class={tab === "saves" ? "active" : ""} 17 + onClick={() => setTab("saves")} 18 + > 19 + Saved 20 + </button> 21 + <button 22 + class={tab === "feeds" ? "active" : ""} 23 + onClick={() => setTab("feeds")} 24 + > 25 + Feeds 26 + </button> 27 + </nav> 28 + </header> 29 + {tab === "saves" ? <SavesView /> : <FeedsView />} 30 + </> 31 + ); 32 + } 33 + 34 + function useAsync<T>(fn: () => Promise<T>, deps: unknown[] = []) { 35 + const [data, setData] = useState<T | null>(null); 36 + const [err, setErr] = useState<string | null>(null); 37 + const [loading, setLoading] = useState(false); 38 + 39 + const reload = async () => { 40 + setLoading(true); 41 + setErr(null); 42 + try { 43 + setData(await fn()); 44 + } catch (e) { 45 + setErr((e as Error).message); 46 + } finally { 47 + setLoading(false); 48 + } 49 + }; 50 + 51 + useEffect(() => { 52 + void reload(); 53 + }, deps); 54 + 55 + return { data, err, loading, reload }; 56 + } 57 + 58 + function SavesView() { 59 + const [unreadOnly, setUnreadOnly] = useState(true); 60 + const { data, err, loading, reload } = useAsync<SavedItem[]>( 61 + () => api.listSaves(!unreadOnly), 62 + [unreadOnly], 63 + ); 64 + 65 + return ( 66 + <> 67 + <AddSave onAdded={reload} /> 68 + <div class="toolbar"> 69 + <div class="filters"> 70 + <label> 71 + <input 72 + type="checkbox" 73 + checked={unreadOnly} 74 + onChange={(e) => 75 + setUnreadOnly((e.target as HTMLInputElement).checked) 76 + } 77 + />{" "} 78 + unread only 79 + </label> 80 + </div> 81 + <button class="ghost" onClick={reload} disabled={loading}> 82 + {loading ? "…" : "refresh"} 83 + </button> 84 + </div> 85 + {err && <div class="error">{err}</div>} 86 + {loading && !data && <div class="loading">loading…</div>} 87 + {data && data.length === 0 && ( 88 + <div class="empty"> 89 + {unreadOnly ? "Nothing unread." : "No saved items."} 90 + </div> 91 + )} 92 + {data && data.length > 0 && ( 93 + <ul class="list"> 94 + {data.map((s) => ( 95 + <SaveRow key={s.id} item={s} onChange={reload} /> 96 + ))} 97 + </ul> 98 + )} 99 + </> 100 + ); 101 + } 102 + 103 + function AddSave({ onAdded }: { onAdded: () => void }) { 104 + const [url, setUrl] = useState(""); 105 + const [busy, setBusy] = useState(false); 106 + const [err, setErr] = useState<string | null>(null); 107 + 108 + const submit = async (e: Event) => { 109 + e.preventDefault(); 110 + if (!url.trim()) return; 111 + setBusy(true); 112 + setErr(null); 113 + try { 114 + await api.saveUrl(url.trim()); 115 + setUrl(""); 116 + onAdded(); 117 + } catch (e) { 118 + setErr((e as Error).message); 119 + } finally { 120 + setBusy(false); 121 + } 122 + }; 123 + 124 + return ( 125 + <> 126 + <form class="add" onSubmit={submit}> 127 + <input 128 + type="url" 129 + placeholder="https://example.com/article-to-save" 130 + value={url} 131 + onInput={(e) => setUrl((e.target as HTMLInputElement).value)} 132 + required 133 + disabled={busy} 134 + /> 135 + <button class="primary" type="submit" disabled={busy}> 136 + {busy ? "saving…" : "save"} 137 + </button> 138 + </form> 139 + {err && <div class="error">{err}</div>} 140 + </> 141 + ); 142 + } 143 + 144 + function SaveRow({ 145 + item, 146 + onChange, 147 + }: { 148 + item: SavedItem; 149 + onChange: () => void; 150 + }) { 151 + const toggleRead = async () => { 152 + await api.markSaveRead(item.id, !item.read); 153 + onChange(); 154 + }; 155 + const remove = async () => { 156 + if (!confirm(`Delete "${item.title}"?`)) return; 157 + await api.deleteSave(item.id); 158 + onChange(); 159 + }; 160 + return ( 161 + <li class={item.read ? "read" : ""}> 162 + <div> 163 + <a class="item-title" href={item.url} target="_blank" rel="noreferrer"> 164 + {item.title || item.url} 165 + </a> 166 + <div class="item-meta"> 167 + {new URL(item.url).hostname} · {formatDate(item.fetchedAt)} 168 + </div> 169 + </div> 170 + <div class="actions"> 171 + <button class="ghost" onClick={toggleRead}> 172 + {item.read ? "mark unread" : "mark read"} 173 + </button> 174 + <button class="danger" onClick={remove} title="Delete"> 175 + 176 + </button> 177 + </div> 178 + </li> 179 + ); 180 + } 181 + 182 + function FeedsView() { 183 + const { data, err, loading, reload } = useAsync<MinifluxFeed[]>( 184 + () => api.listFeeds(), 185 + [], 186 + ); 187 + const [refreshing, setRefreshing] = useState(false); 188 + 189 + const refreshAll = async () => { 190 + setRefreshing(true); 191 + try { 192 + await api.refreshAll(); 193 + } finally { 194 + setRefreshing(false); 195 + reload(); 196 + } 197 + }; 198 + 199 + return ( 200 + <> 201 + <AddFeed onAdded={reload} /> 202 + <div class="toolbar"> 203 + <div class="filters">{data ? `${data.length} subscribed` : ""}</div> 204 + <div class="actions"> 205 + <button class="ghost" onClick={refreshAll} disabled={refreshing}> 206 + {refreshing ? "refreshing…" : "refresh all"} 207 + </button> 208 + <button class="ghost" onClick={reload} disabled={loading}> 209 + reload 210 + </button> 211 + </div> 212 + </div> 213 + {err && <div class="error">{err}</div>} 214 + {loading && !data && <div class="loading">loading…</div>} 215 + {data && data.length === 0 && ( 216 + <div class="empty">No feeds subscribed.</div> 217 + )} 218 + {data && data.length > 0 && ( 219 + <ul class="list"> 220 + {data.map((f) => ( 221 + <FeedRow key={f.id} feed={f} onChange={reload} /> 222 + ))} 223 + </ul> 224 + )} 225 + </> 226 + ); 227 + } 228 + 229 + function AddFeed({ onAdded }: { onAdded: () => void }) { 230 + const [url, setUrl] = useState(""); 231 + const [busy, setBusy] = useState(false); 232 + const [err, setErr] = useState<string | null>(null); 233 + 234 + const submit = async (e: Event) => { 235 + e.preventDefault(); 236 + if (!url.trim()) return; 237 + setBusy(true); 238 + setErr(null); 239 + try { 240 + await api.subscribe(url.trim()); 241 + setUrl(""); 242 + onAdded(); 243 + } catch (e) { 244 + setErr((e as Error).message); 245 + } finally { 246 + setBusy(false); 247 + } 248 + }; 249 + 250 + return ( 251 + <> 252 + <form class="add" onSubmit={submit}> 253 + <input 254 + type="url" 255 + placeholder="https://example.com/feed.xml" 256 + value={url} 257 + onInput={(e) => setUrl((e.target as HTMLInputElement).value)} 258 + required 259 + disabled={busy} 260 + /> 261 + <button class="primary" type="submit" disabled={busy}> 262 + {busy ? "subscribing…" : "subscribe"} 263 + </button> 264 + </form> 265 + {err && <div class="error">{err}</div>} 266 + </> 267 + ); 268 + } 269 + 270 + function FeedRow({ 271 + feed, 272 + onChange, 273 + }: { 274 + feed: MinifluxFeed; 275 + onChange: () => void; 276 + }) { 277 + const remove = async () => { 278 + if (!confirm(`Unsubscribe from "${feed.title}"?`)) return; 279 + await api.unsubscribe(feed.id); 280 + onChange(); 281 + }; 282 + return ( 283 + <li> 284 + <div> 285 + <a 286 + class="item-title" 287 + href={feed.site_url || feed.feed_url} 288 + target="_blank" 289 + rel="noreferrer" 290 + > 291 + {feed.title} 292 + </a> 293 + <div class="item-meta"> 294 + {feed.feed_url} 295 + {feed.category?.title ? ` · ${feed.category.title}` : ""} 296 + </div> 297 + </div> 298 + <div class="actions"> 299 + <button class="danger" onClick={remove} title="Unsubscribe"> 300 + 301 + </button> 302 + </div> 303 + </li> 304 + ); 305 + } 306 + 307 + function formatDate(unix: number): string { 308 + const d = new Date(unix * 1000); 309 + return d.toLocaleDateString(undefined, { 310 + year: "numeric", 311 + month: "short", 312 + day: "numeric", 313 + }); 314 + }
+49
src/web/api.ts
··· 1 + import type { MinifluxFeed, SavedItem } from "../shared/types.js"; 2 + 3 + async function req<T>(path: string, init?: RequestInit): Promise<T> { 4 + const res = await fetch(path, { 5 + ...init, 6 + headers: { 7 + "Content-Type": "application/json", 8 + ...(init?.headers ?? {}), 9 + }, 10 + }); 11 + if (!res.ok) { 12 + const text = await res.text().catch(() => ""); 13 + let msg = text; 14 + try { 15 + const parsed = JSON.parse(text); 16 + msg = parsed.error ?? text; 17 + } catch {} 18 + throw new Error(msg || `${res.status} ${res.statusText}`); 19 + } 20 + if (res.status === 204) return undefined as T; 21 + const ct = res.headers.get("content-type") ?? ""; 22 + if (ct.includes("application/json")) return res.json() as Promise<T>; 23 + return undefined as T; 24 + } 25 + 26 + export const api = { 27 + listSaves: (all: boolean): Promise<SavedItem[]> => 28 + req(`/api/saves${all ? "?all=1" : ""}`), 29 + 30 + saveUrl: (url: string): Promise<{ id: number; title: string }> => 31 + req("/api/saves", { method: "POST", body: JSON.stringify({ url }) }), 32 + 33 + deleteSave: (id: number): Promise<void> => 34 + req(`/api/saves/${id}`, { method: "DELETE" }), 35 + 36 + markSaveRead: (id: number, read: boolean): Promise<void> => 37 + req(`/api/saves/${id}/${read ? "read" : "unread"}`, { method: "POST" }), 38 + 39 + listFeeds: (): Promise<MinifluxFeed[]> => req("/api/feeds"), 40 + 41 + subscribe: (url: string): Promise<{ feed_id: number }> => 42 + req("/api/feeds", { method: "POST", body: JSON.stringify({ url }) }), 43 + 44 + unsubscribe: (id: number): Promise<void> => 45 + req(`/api/feeds/${id}`, { method: "DELETE" }), 46 + 47 + refreshAll: (): Promise<void> => 48 + req("/api/feeds/refresh", { method: "POST" }), 49 + };
+13
src/web/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <title>Nightshade</title> 7 + <link rel="stylesheet" href="./styles.css" /> 8 + </head> 9 + <body> 10 + <div id="app"></div> 11 + <script type="module" src="./main.tsx"></script> 12 + </body> 13 + </html>
+5
src/web/main.tsx
··· 1 + import { render } from "preact"; 2 + import { App } from "./App.js"; 3 + 4 + const root = document.getElementById("app"); 5 + if (root) render(<App />, root);
+208
src/web/styles.css
··· 1 + :root { 2 + --bg: #faf8f3; 3 + --fg: #1c1b17; 4 + --muted: #6b6860; 5 + --accent: #3a5a40; 6 + --border: #d8d2c3; 7 + --danger: #9a3b2a; 8 + --card: #fffefa; 9 + } 10 + 11 + * { box-sizing: border-box; } 12 + 13 + html, body { 14 + margin: 0; 15 + padding: 0; 16 + font-family: ui-serif, Georgia, "Times New Roman", serif; 17 + background: var(--bg); 18 + color: var(--fg); 19 + -webkit-font-smoothing: antialiased; 20 + } 21 + 22 + body { 23 + max-width: 52rem; 24 + margin: 0 auto; 25 + padding: 2rem 1rem 4rem; 26 + line-height: 1.5; 27 + } 28 + 29 + header { 30 + display: flex; 31 + align-items: baseline; 32 + justify-content: space-between; 33 + margin-bottom: 2rem; 34 + border-bottom: 1px solid var(--border); 35 + padding-bottom: 1rem; 36 + } 37 + 38 + header h1 { 39 + margin: 0; 40 + font-weight: 600; 41 + font-size: 1.75rem; 42 + letter-spacing: -0.01em; 43 + } 44 + 45 + nav { 46 + display: flex; 47 + gap: 1.5rem; 48 + } 49 + 50 + nav button { 51 + background: none; 52 + border: none; 53 + font: inherit; 54 + cursor: pointer; 55 + padding: 0.25rem 0; 56 + color: var(--muted); 57 + border-bottom: 2px solid transparent; 58 + } 59 + 60 + nav button.active { 61 + color: var(--fg); 62 + border-bottom-color: var(--accent); 63 + } 64 + 65 + form.add { 66 + display: flex; 67 + gap: 0.5rem; 68 + margin-bottom: 1.5rem; 69 + } 70 + 71 + input[type="url"], input[type="text"] { 72 + flex: 1; 73 + padding: 0.5rem 0.75rem; 74 + border: 1px solid var(--border); 75 + border-radius: 3px; 76 + background: var(--card); 77 + font: inherit; 78 + color: var(--fg); 79 + } 80 + 81 + input[type="url"]:focus, input[type="text"]:focus { 82 + outline: none; 83 + border-color: var(--accent); 84 + } 85 + 86 + button.primary, button.ghost, button.danger { 87 + padding: 0.5rem 0.85rem; 88 + border: 1px solid var(--border); 89 + border-radius: 3px; 90 + font: inherit; 91 + cursor: pointer; 92 + background: var(--card); 93 + color: var(--fg); 94 + white-space: nowrap; 95 + } 96 + 97 + button.primary { 98 + background: var(--accent); 99 + color: #fff; 100 + border-color: var(--accent); 101 + } 102 + 103 + button.primary:hover:not(:disabled), button.primary:focus { 104 + filter: brightness(1.1); 105 + } 106 + 107 + button.ghost:hover, button.ghost:focus { 108 + background: var(--bg); 109 + } 110 + 111 + button.danger { 112 + color: var(--danger); 113 + border-color: transparent; 114 + background: transparent; 115 + padding: 0.25rem 0.5rem; 116 + } 117 + 118 + button.danger:hover { 119 + background: rgba(154, 59, 42, 0.08); 120 + } 121 + 122 + button:disabled { 123 + opacity: 0.5; 124 + cursor: not-allowed; 125 + } 126 + 127 + .toolbar { 128 + display: flex; 129 + align-items: center; 130 + justify-content: space-between; 131 + margin-bottom: 1rem; 132 + gap: 1rem; 133 + } 134 + 135 + .filters { 136 + display: flex; 137 + gap: 0.75rem; 138 + align-items: center; 139 + color: var(--muted); 140 + font-size: 0.9rem; 141 + } 142 + 143 + .filters label { 144 + cursor: pointer; 145 + } 146 + 147 + ul.list { 148 + list-style: none; 149 + padding: 0; 150 + margin: 0; 151 + } 152 + 153 + ul.list li { 154 + padding: 0.9rem 0; 155 + border-bottom: 1px solid var(--border); 156 + display: grid; 157 + grid-template-columns: 1fr auto; 158 + gap: 0.5rem 1rem; 159 + align-items: start; 160 + } 161 + 162 + ul.list li.read { 163 + opacity: 0.55; 164 + } 165 + 166 + .item-title { 167 + font-weight: 500; 168 + color: var(--fg); 169 + text-decoration: none; 170 + } 171 + 172 + .item-title:hover { 173 + text-decoration: underline; 174 + } 175 + 176 + .item-meta { 177 + color: var(--muted); 178 + font-size: 0.85rem; 179 + margin-top: 0.15rem; 180 + word-break: break-all; 181 + } 182 + 183 + .actions { 184 + display: flex; 185 + gap: 0.25rem; 186 + align-items: center; 187 + } 188 + 189 + .empty { 190 + color: var(--muted); 191 + font-style: italic; 192 + padding: 2rem 0; 193 + text-align: center; 194 + } 195 + 196 + .error { 197 + background: rgba(154, 59, 42, 0.1); 198 + color: var(--danger); 199 + padding: 0.75rem 1rem; 200 + border-radius: 3px; 201 + margin-bottom: 1rem; 202 + border-left: 3px solid var(--danger); 203 + } 204 + 205 + .loading { 206 + color: var(--muted); 207 + padding: 1rem 0; 208 + }
+24
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "Bundler", 6 + "strict": true, 7 + "noUncheckedIndexedAccess": true, 8 + "esModuleInterop": true, 9 + "skipLibCheck": true, 10 + "resolveJsonModule": true, 11 + "allowSyntheticDefaultImports": true, 12 + "forceConsistentCasingInFileNames": true, 13 + "jsx": "react-jsx", 14 + "jsxImportSource": "preact", 15 + "lib": ["ES2022", "DOM", "DOM.Iterable"], 16 + "types": ["node"], 17 + "baseUrl": ".", 18 + "paths": { 19 + "@shared/*": ["src/shared/*"] 20 + } 21 + }, 22 + "include": ["src/**/*"], 23 + "exclude": ["node_modules", "dist"] 24 + }
+18
vite.config.ts
··· 1 + import { defineConfig } from "vite"; 2 + import preact from "@preact/preset-vite"; 3 + 4 + export default defineConfig({ 5 + plugins: [preact()], 6 + root: "src/web", 7 + build: { 8 + outDir: "../../dist/public", 9 + emptyOutDir: true, 10 + }, 11 + server: { 12 + port: 5173, 13 + proxy: { 14 + "/api": "http://localhost:8787", 15 + "/device": "http://localhost:8787", 16 + }, 17 + }, 18 + });