diff --git a/.gitignore b/.gitignore index ef07c1c..be509ad 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,11 @@ yarn-error.log* .superpowers/ # docs/superpowers -/docs/superpowers \ No newline at end of file +/docs/superpowers + +# git worktrees +.worktrees/ + +# stray +post.md +plugins/decky-vault/dist/ diff --git a/README.md b/README.md index 81a807c..050d9b0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![Live Site](https://img.shields.io/badge/Live-deckyvault.xyz-eb3779?style=flat-square)](https://deckyvault.xyz) [![Next.js](https://img.shields.io/badge/Next.js-16-black?style=flat-square&logo=next.js)](https://nextjs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org) +[![Bun](https://img.shields.io/badge/Bun-1.3-black?style=flat-square&logo=bun)](https://bun.sh) [![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](#license) @@ -15,6 +16,8 @@ DeckyVault is an open-source platform where the Steam Deck community shares real-world performance benchmarks, optimized game settings, and compatibility reports. Every data point comes from actual players — not spec sheets. +The project includes a **Decky Loader plugin** that automatically records performance metrics (FPS, TDP, temps) during gameplay and exports or uploads them directly to DeckyVault. + > **Actively developed.** The site is live at [deckyvault.xyz](https://deckyvault.xyz). Features ship incrementally. ## Features @@ -45,6 +48,40 @@ DeckyVault is an open-source platform where the Steam Deck community shares real - **Saved games** — bookmark and track the games you care about - **Admin dashboard** — moderation tools for comments, reports, and content management +### Decky Loader Plugin +- **Auto-record performance** — MangoHud-powered FPS, TDP, and temperature logging +- **One-click upload** — send benchmarks directly to DeckyVault via API key +- **Export to file** — save `.deckyvault.json` files for manual upload +- **Manual inputs** — upscaler type, frame gen, in-game settings, load times, notes +- **Hardware auto-detection** — identifies Steam Deck LCD vs OLED from DMI data + +## Monorepo Structure + +``` +deckyvault/ +├── apps/ +│ └── web/ # Next.js 16 web application +│ ├── app/ # App Router pages & API routes +│ ├── components/ # React components +│ ├── lib/ # API routes, auth, DB schema +│ └── drizzle/ # Database migrations +├── packages/ +│ └── shared/ # Shared TypeScript types & constants +│ └── src/ +│ └── index.ts # DeckyVaultImportV1, hardware slugs, API types +├── plugins/ +│ └── decky-vault/ # Decky Loader plugin +│ ├── main.py # Python backend (filesystem, shell, HTTP) +│ ├── src/ # TypeScript/React frontend +│ │ ├── index.tsx # Plugin entry point (definePlugin) +│ │ ├── components/ # Main panel, session form, settings panel +│ │ └── lib/ # RPC wrappers, state management +│ └── tests/ # Python unit tests +├── docs/ +│ └── superpowers/ # Plans & specs +└── public/ # Static assets +``` + ## Architecture ``` @@ -66,13 +103,27 @@ DeckyVault is an open-source platform where the Steam Deck community shares real │ Drizzle ORM → PostgreSQL │ │ lib/db/schema/ — 9 schema files │ ├─────────────────────────────────────────────────┤ -│ better-auth (Google + Discord OAuth, Passkeys) │ -│ AWS S3 (file storage) · Resend (email) │ +│ better-auth (Google + Discord OAuth, Passkeys, │ +│ API Keys) · AWS S3 · Resend │ └─────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────┐ +│ Decky Loader Plugin │ +│ ┌─────────────────┐ ┌──────────────────────┐ │ +│ │ React Frontend │ │ Python Backend │ │ +│ │ (Steam CEF) │◄─┤ (filesystem, shell, │ │ +│ │ UI + state │ │ HTTP, log parsing) │ │ +│ └─────────────────┘ └──────────┬───────────┘ │ +│ │ │ +│ POST /api/performance/import│ +│ (via x-api-key header) │ +└─────────────────────────────────────────────────────┘ ``` The API layer uses [Elysia](https://elysiajs.com) mounted as a catch-all Next.js route handler at `app/api/[[...slugs]]/route.ts`. All route modules live in `lib/api/` and are composed into a single Elysia app. +The Decky Loader plugin uses a dual architecture: a React/TypeScript frontend (runs in Steam's CEF context) communicates with a Python backend via `@decky/api`'s RPC mechanism. The Python backend handles filesystem I/O, shell commands, and HTTP requests to the DeckyVault API. + ## Tech Stack | Layer | Technology | @@ -82,13 +133,15 @@ The API layer uses [Elysia](https://elysiajs.com) mounted as a catch-all Next.js | Language | [TypeScript](https://www.typescriptlang.org) | | API | [Elysia](https://elysiajs.com) | | Database | [PostgreSQL](https://www.postgresql.org) · [Drizzle ORM](https://orm.drizzle.team) | -| Auth | [better-auth](https://better-auth.com) (Google, Discord, Passkeys, OTP) | +| Auth | [better-auth](https://better-auth.com) (Google, Discord, Passkeys, OTP, API Keys) | | Editor | [Tiptap](https://tiptap.dev) (rich text) | | Animations | [Motion](https://motion.dev) | | Charts | [ECharts](https://echarts.apache.org) | | Storage | [AWS S3](https://aws.amazon.com/s3/) | | Email | [Resend](https://resend.com) | | Runtime | [Bun](https://bun.sh) | +| Plugin SDK | [@decky/api](https://npmjs.com/package/@decky/api) · [@decky/ui](https://npmjs.com/package/@decky/ui) | +| Plugin Backend | Python 3 · urllib (stdlib) | ## Getting Started @@ -111,7 +164,7 @@ bun install Copy the example environment file and fill in your values: ```bash -cp .env.example .env.local +cp apps/web/.env.example apps/web/.env.local ``` Required variables: @@ -135,6 +188,7 @@ Required variables: Push the schema to your database: ```bash +cd apps/web bun run db:push ``` @@ -154,6 +208,7 @@ bun run db:seed ### Development ```bash +# From the root — runs the web app bun run dev ``` @@ -163,7 +218,7 @@ Open [https://localhost:3000](https://localhost:3000) (self-signed HTTPS via `-- | Command | Description | |---------|-------------| -| `bun run dev` | Start dev server with HTTPS | +| `bun run dev` | Start web app dev server with HTTPS | | `bun run build` | Create production build | | `bun run start` | Start production server | | `bun run lint` | Run ESLint | @@ -175,46 +230,119 @@ Open [https://localhost:3000](https://localhost:3000) (self-signed HTTPS via `-- | `bun run db:studio` | Open Drizzle Studio | | `bun run db:seed` | Seed the database | +## Decky Loader Plugin + +The `plugins/decky-vault/` directory contains a Decky Loader plugin that records performance metrics and exports/uploads them to DeckyVault. + +### Building + +```bash +cd plugins/decky-vault +bun install +bun run build +``` + +Output: `plugins/decky-vault/dist/index.js` + +### Installing on Steam Deck + +1. Build the plugin (see above) +2. Copy the entire `plugins/decky-vault/` directory to `/home/deck/homebrew/plugins/` on your Steam Deck +3. Restart Decky Loader or reload plugins +4. The plugin appears as "DeckyVault" in the Quick Access Menu + +### Usage + +1. Open the DeckyVault plugin from the Quick Access Menu (QAM) +2. Go to the **Settings** tab and configure: + - **API Key** — get one from DeckyVault → Profile → Settings → API Keys + - **Write MangoHud Config** — writes the logging config to `~/.config/MangoHud/MangoHud.conf` +3. Add `mangohud %command%` to your game's Steam launch options +4. Go to the **Record** tab and press **Start Recording** +5. Play your game +6. Press **Stop Recording** — FPS stats are parsed from the MangoHud log +7. Fill in manual details (upscaler, frame gen, settings, etc.) +8. **Export to File** or **Upload to DeckyVault** + +### Plugin Architecture + +``` +plugins/decky-vault/ +├── main.py # Python backend — settings, MangoHud, system info, export, upload +├── src/ +│ ├── index.tsx # Entry point — definePlugin, SteamClient events, tab nav +│ ├── types.d.ts # SteamClient type declarations +│ ├── lib/ +│ │ ├── api.ts # Typed RPC wrappers (callable → Python methods) +│ │ └── store.ts # React hooks (useSettings, useSession) + payload builder +│ └── components/ +│ ├── main-panel.tsx # Record/Stop button, timer, recent recordings +│ ├── session-form.tsx # Auto-captured metrics + manual inputs + actions +│ └── settings-panel.tsx # API key, export path, hardware, MangoHud setup +├── tests/ +│ ├── test_mangohud_parser.py # 7 unit tests for log parsing +│ ├── test_settings.py # 4 unit tests for settings persistence +│ └── fixtures/ +│ └── sample_mangohud.log # Sample log for parser tests +├── package.json # Frontend deps (@decky/api, @decky/ui, @decky/rollup) +├── plugin.json # Decky plugin metadata +└── rollup.config.js # @decky/rollup build config +``` + +### Running Plugin Tests + +```bash +cd plugins/decky-vault +python -m pytest tests/ -v +``` + ## Project Structure ``` deckyvault/ -├── app/ -│ ├── (auth)/ # Auth pages (sign-in, reset password) -│ ├── (manage)/manage/ # Admin dashboard -│ │ ├── benchmarks/ # Benchmark moderation -│ │ ├── comments/ # Comment moderation -│ │ ├── games/ # Game management & sync -│ │ ├── hardware/ # Hardware management -│ │ ├── reports/ # Report moderation -│ │ └── users/ # User management -│ ├── api/[[...slugs]]/ # Elysia API catch-all -│ ├── compare/ # Side-by-side game comparison -│ ├── game/[id]/ # Individual game page -│ ├── games/ # Games listing -│ ├── devices/ # Hardware device pages -│ ├── profile/ # User profiles -│ └── search/ # Unified search -├── components/ -│ ├── auth/ # Auth-related components -│ ├── charts/ # ECharts wrappers -│ ├── comments/ # CommentSection, CommentItem -│ ├── manage/ # Admin sidebar -│ ├── profile/ # Settings tabs -│ └── wizard/ # Contribution wizard -├── lib/ -│ ├── api/ # 24 Elysia route modules -│ ├── auth.ts # better-auth server config -│ ├── auth-client.ts # better-auth client -│ ├── db/ -│ │ ├── schema/ # 9 Drizzle schema files -│ │ ├── index.ts # DB connection -│ │ └── seed.ts # Database seeder -│ ├── hooks/ # Custom React hooks -│ └── steam/ # Steam API integration -├── drizzle/ # Generated migrations -├── docs/superpowers/ # Plans & specs -└── public/ # Static assets +├── apps/ +│ └── web/ +│ ├── app/ +│ │ ├── (auth)/ # Auth pages (sign-in, reset password) +│ │ ├── (manage)/manage/ # Admin dashboard +│ │ │ ├── benchmarks/ # Benchmark moderation +│ │ │ ├── comments/ # Comment moderation +│ │ │ ├── games/ # Game management & sync +│ │ │ ├── hardware/ # Hardware management +│ │ │ ├── reports/ # Report moderation +│ │ │ └── users/ # User management +│ │ ├── api/[[...slugs]]/ # Elysia API catch-all +│ │ ├── compare/ # Side-by-side game comparison +│ │ ├── game/[id]/ # Individual game page +│ │ ├── games/ # Games listing +│ │ ├── devices/ # Hardware device pages +│ │ ├── profile/ # User profiles +│ │ └── search/ # Unified search +│ ├── components/ +│ │ ├── auth/ # Auth-related components +│ │ ├── charts/ # ECharts wrappers +│ │ ├── comments/ # CommentSection, CommentItem +│ │ ├── manage/ # Admin sidebar +│ │ ├── profile/ # Settings tabs +│ │ └── wizard/ # Contribution wizard +│ ├── lib/ +│ │ ├── api/ # 24 Elysia route modules +│ │ ├── auth.ts # better-auth server config +│ │ ├── auth-client.ts # better-auth client +│ │ ├── db/ +│ │ │ ├── schema/ # 9 Drizzle schema files +│ │ │ ├── index.ts # DB connection +│ │ │ └── seed.ts # Database seeder +│ │ ├── hooks/ # Custom React hooks +│ │ └── steam/ # Steam API integration +│ └── drizzle/ # Generated migrations +├── packages/ +│ └── shared/ # Shared types & constants +├── plugins/ +│ └── decky-vault/ # Decky Loader plugin +├── docs/ +│ └── superpowers/ # Plans & specs +└── public/ # Static assets ``` ## Contributing @@ -244,4 +372,4 @@ Created by [Adrian Bonpin](https://grounds.ph). --- -*DeckyVault is an independent project and is not affiliated with Valve Corporation.* +*DeckyVault is an independent project and is not affiliated with Valve Corporation.* \ No newline at end of file diff --git a/apps/web/components/auth/login-form.tsx b/apps/web/components/auth/login-form.tsx index 8dbfddd..50cd0da 100644 --- a/apps/web/components/auth/login-form.tsx +++ b/apps/web/components/auth/login-form.tsx @@ -57,23 +57,32 @@ export default function LoginForm() { // Preload passkeys for conditional UI — must be called on mount when // both email + password fields are in the DOM. + // Note: WebAuthn conditional mediation can throw NetworkError on pages + // with self-signed certs (common in dev). We catch and suppress it. useEffect(() => { mountedRef.current = true if ("PublicKeyCredential" in window && !passkeyInitiatedRef.current) { passkeyInitiatedRef.current = true suppressPasskeyErrors = true - authClient.signIn.passkey({ - autoFill: true, - fetchOptions: { - onSuccess: handleLoginSuccess, - }, - }).catch((err) => { - if (!isWebAuthnAbortError(err)) { - console.warn("[passkey-conditional-ui]", err) + + const startPasskeyAutoFill = async () => { + try { + await authClient.signIn.passkey({ + autoFill: true, + fetchOptions: { + onSuccess: handleLoginSuccess, + }, + }) + } catch (err) { + if (!isWebAuthnAbortError(err)) { + console.warn("[passkey-conditional-ui]", err) + } + } finally { + suppressPasskeyErrors = false } - }).finally(() => { - suppressPasskeyErrors = false - }) + } + + startPasskeyAutoFill() } return () => { mountedRef.current = false } }, [handleLoginSuccess]) diff --git a/apps/web/lib/api/games-lookup.ts b/apps/web/lib/api/games-lookup.ts index b4225e9..c889715 100644 --- a/apps/web/lib/api/games-lookup.ts +++ b/apps/web/lib/api/games-lookup.ts @@ -11,7 +11,7 @@ export const gamesLookupRoutes = new Elysia({ async ({ query, set }) => { const { steamAppId } = query - if (!steamAppId) { + if (steamAppId === undefined || steamAppId === null) { set.status = 400 return { error: "steamAppId query parameter is required" } } diff --git a/apps/web/package.json b/apps/web/package.json index 0aafd7c..5d4c4dd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -37,7 +37,7 @@ "@tiptap/pm": "^3.22.4", "@tiptap/react": "^3.22.4", "@tiptap/starter-kit": "^3.22.4", - "better-auth": "^1.6.9", + "better-auth": "^1.6.22", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "echarts": "^6.0.0", @@ -59,7 +59,8 @@ "resend": "^6.12.2", "serwist": "^9.5.11", "sharp": "^0.34.5", - "web-haptics": "^0.0.6" + "web-haptics": "^0.0.6", + "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/bun.lock b/bun.lock index 3a6682f..f4be83f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "deckyvault", + "dependencies": { + "better-auth": "^1.6.22", + }, }, "apps/web": { "name": "@deckyvault/web", @@ -29,7 +32,7 @@ "@tiptap/pm": "^3.22.4", "@tiptap/react": "^3.22.4", "@tiptap/starter-kit": "^3.22.4", - "better-auth": "^1.6.9", + "better-auth": "^1.6.22", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "echarts": "^6.0.0", @@ -52,6 +55,7 @@ "serwist": "^9.5.11", "sharp": "^0.34.5", "web-haptics": "^0.0.6", + "zod": "^4.4.3", }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -81,13 +85,18 @@ "name": "@deckyvault/plugin", "version": "0.1.0", "dependencies": { + "@decky/api": "^1.1.3", "@deckyvault/shared": "workspace:*", + "react-icons": "^5.3.0", + "tslib": "^2.7.0", }, "devDependencies": { - "@rollup/plugin-commonjs": "^28", - "@rollup/plugin-typescript": "^12", - "rollup": "^4", - "typescript": "^5", + "@decky/rollup": "^1.0.2", + "@decky/ui": "^4.11.6", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "rollup": "^4.53.3", + "typescript": "^5.6.2", }, }, }, @@ -216,30 +225,36 @@ "@better-auth/api-key": ["@better-auth/api-key@1.6.22", "", { "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "better-auth": "^1.6.22", "better-call": "1.3.7" } }, "sha512-HDiiLYF0ov0zqhKv4CMTyLwpjTZ3UWl2dug451uTw40VM9zGxWSNxwILDcMDZ6hS5evaTHmmk7R5gciskEk2nQ=="], - "@better-auth/core": ["@better-auth/core@1.6.9", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w=="], + "@better-auth/core": ["@better-auth/core@1.6.22", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-aFH/5nzmR501jAJPKjJfiVg4BrkcjVCqq9WS9JnhTruE/2PIWopv1QGMiRIRqxXaPbHczri7cqRdhep3Lg5PMw=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-uNa9qH53CfxBmuKP8kbLWxY90oIRwUPn5BcHhO+szK05e2yh6EYwSNNivDSqV3YG5HPfjtPHulklInjq1wtm3w=="], "@better-auth/expo": ["@better-auth/expo@1.6.11", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "better-call": "1.3.5", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.11", "better-auth": "^1.6.11", "expo-constants": ">=17.0.0", "expo-linking": ">=7.0.0", "expo-network": ">=8.0.7", "expo-web-browser": ">=14.0.0" }, "optionalPeers": ["expo-constants", "expo-linking", "expo-network", "expo-web-browser"] }, "sha512-ahqtpj5DRF4Tu8+PZuLPkR10Q6b8AntQNsn4LPcOIp6za5IJDAsaD/go1I5qCYIRi+8YKiIXk9vh4qQM54u+hA=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-4k/07lPRizlQi+B+uOE5CwTfH3w+Lq8ZDX1nDN1+e+glRVKAIfHoLvC9cfAVcCbio3DDrl0RTbwjLwjEhG0LxA=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0" } }, "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2" } }, "sha512-rbepe/gHhWs0aF4fAu6+l+wNPJxT9XN4U+Hqa1Y/5HhjtT9y5evo3INrSWlkIOymsMaQ0cBPrSL5pm9Z195hcA=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-OYnfySHlVkIx7y6XNBsCHjKhl6IYGprRkFwifc/TAuPBVjoRKhLKRAXuzMdWTQYFt4FYb6holbhjNUrXxPIWcw=="], "@better-auth/passkey": ["@better-auth/passkey@1.6.9", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.9", "better-call": "1.3.5", "nanostores": "^1.0.1" } }, "sha512-MFpi+2G/pG2wVcTuL/PcnWxP2ddFL4jmFByTCbgvr61tp7u96d5liBptxpTqfS5IuCi2o8bBRmjiQnDZAvxmHg=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-I6lWQwLva732V600u5dLM2kRcQ94pRZOVVfZ87+Ow9RBxMUnV+I+YQ5h2yggdN2tmsmITacZTy1DSZbDxGu0LQ=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.22", "", { "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-glq/oEk9qP+zGh9k/WUH5+pwvBCMolNNhaAVBCtYQrkADFee2gP3VoPs1YeO9coNuOmBhc+AYSIHs+fL9DoJnw=="], - "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], - "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@decky/api": ["@decky/api@1.1.3", "", {}, "sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA=="], + + "@decky/rollup": ["@decky/rollup@1.0.2", "", { "dependencies": { "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-typescript": "^11.1.6", "merge-anything": "^6.0.2", "rollup": "^4.18.0", "rollup-plugin-delete": "^2.0.0", "rollup-plugin-external-globals": "^0.11.0", "rollup-plugin-import-assets": "^1.1.1", "tslib": "^2.6.3", "typescript": "^5.5.3" } }, "sha512-ixuHH3msw156ACbgWZi4hgccdzDBIuqYGOVja8sLX4lHFgaWUKji1vomDi7Dk1CamgjmKt+Dqf75Pezv2FB6Bw=="], + + "@decky/ui": ["@decky/ui@4.11.6", "", {}, "sha512-vPCr2/KODeM6DAzIL/XN2e/RY7vhebXoWoh8e0VvB5QJU59Usb1z/cIpNmqe/GEMd1P3om6DFMcpEW5v8Se95Q=="], + "@deckyvault/plugin": ["@deckyvault/plugin@workspace:plugins/decky-vault"], "@deckyvault/shared": ["@deckyvault/shared@workspace:packages/shared"], @@ -402,6 +417,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -478,6 +495,8 @@ "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], @@ -510,9 +529,15 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], - "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@26.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "glob": "^10.4.1", "is-reference": "1.2.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ=="], - "@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big=="], + "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@5.0.7", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ=="], + + "@rollup/plugin-typescript": ["@rollup/plugin-typescript@11.1.6", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], @@ -822,6 +847,8 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -906,8 +933,12 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], @@ -918,6 +949,8 @@ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], @@ -950,9 +983,9 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA=="], - "better-auth": ["better-auth@1.6.9", "", { "dependencies": { "@better-auth/core": "1.6.9", "@better-auth/drizzle-adapter": "1.6.9", "@better-auth/kysely-adapter": "1.6.9", "@better-auth/memory-adapter": "1.6.9", "@better-auth/mongo-adapter": "1.6.9", "@better-auth/prisma-adapter": "1.6.9", "@better-auth/telemetry": "1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA=="], + "better-auth": ["better-auth@1.6.22", "", { "dependencies": { "@better-auth/core": "1.6.22", "@better-auth/drizzle-adapter": "1.6.22", "@better-auth/kysely-adapter": "1.6.22", "@better-auth/memory-adapter": "1.6.22", "@better-auth/mongo-adapter": "1.6.22", "@better-auth/prisma-adapter": "1.6.22", "@better-auth/telemetry": "1.6.22", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-B5s6+lPsDWp8rGLRnvNyr5h9tftG9zLRjNrlkEJdYRhcuhPhJiw9b8o6ibgxEFpSAdUqoDaP5/FLqfu8QsXIVg=="], - "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -986,6 +1019,8 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -1026,18 +1061,24 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "del": ["del@6.1.1", "", { "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.2", "p-map": "^4.0.0", "rimraf": "^3.0.2", "slash": "^3.0.0" } }, "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -1048,6 +1089,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "echarts": ["echarts@6.0.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" } }, "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ=="], "echarts-for-react": ["echarts-for-react@3.0.6", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "size-sensor": "^1.0.1" }, "peerDependencies": { "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "react": "^15.0.0 || >=16.0.0" } }, "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg=="], @@ -1164,8 +1207,12 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1196,6 +1243,8 @@ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -1242,6 +1291,12 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], @@ -1268,18 +1323,26 @@ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-path-cwd": ["is-path-cwd@2.2.0", "", {}, "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], @@ -1302,12 +1365,16 @@ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], @@ -1334,7 +1401,7 @@ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], - "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="], + "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], @@ -1398,6 +1465,8 @@ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "merge-anything": ["merge-anything@6.0.6", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-F3K1W45PvTjRZzbcYIhXntNr8cux00gUxR8IzNPPG+80gNlAHZGVBwFyN4x5yjw/7QkLPKDbRQBK4KrJKo69mw=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -1490,6 +1559,8 @@ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1502,18 +1573,26 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], @@ -1630,10 +1709,20 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + "rollup-plugin-delete": ["rollup-plugin-delete@2.2.0", "", { "dependencies": { "del": "^6.1.1" }, "peerDependencies": { "rollup": "*" } }, "sha512-REKtDKWvjZlbrWpPvM9X/fadCs3E9I9ge27AK8G0e4bXwSLeABAAwtjiI1u3ihqZxk6mJeB2IVeSbH4DtOcw7A=="], + + "rollup-plugin-external-globals": ["rollup-plugin-external-globals@0.11.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "estree-walker": "^3.0.3", "is-reference": "^3.0.2", "magic-string": "^0.30.10" }, "peerDependencies": { "rollup": "^2.25.0 || ^3.3.0 || ^4.1.4" } }, "sha512-LR+sH2WkgWMPxsA5o5rT7uW7BeWXSeygLe60QQi9qoN/ufaCuHDaVOIbndIkqDPnZt/wZugJh5DCzkZFdSWlLQ=="], + + "rollup-plugin-import-assets": ["rollup-plugin-import-assets@1.1.1", "", { "dependencies": { "rollup-pluginutils": "^2.7.1", "url-join": "^4.0.1" }, "peerDependencies": { "rollup": ">=1.9.0" } }, "sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1678,8 +1767,12 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "size-sensor": ["size-sensor@1.0.3", "", {}, "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1702,6 +1795,10 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1716,6 +1813,10 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], @@ -1808,6 +1909,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1844,13 +1947,19 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -1872,7 +1981,21 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@better-auth/api-key/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@better-auth/core/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@better-auth/expo/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@better-auth/expo/better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + + "@better-auth/expo/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@better-auth/passkey/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + + "@better-auth/passkey/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@better-auth/passkey/better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], "@better-auth/passkey/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -1886,6 +2009,14 @@ "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@rollup/plugin-commonjs/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "@serwist/build/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@serwist/next/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@serwist/webpack-plugin/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1906,7 +2037,7 @@ "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "better-auth/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], "echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], @@ -1938,12 +2069,32 @@ "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "rollup-plugin-external-globals/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "rollup-plugin-external-globals/is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -1952,6 +2103,8 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@better-auth/expo/better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1998,12 +2151,20 @@ "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@rollup/plugin-commonjs/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@rollup/plugin-commonjs/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@vitest/mocker/estree-walker/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "rollup-plugin-external-globals/estree-walker/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], @@ -2056,12 +2217,20 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@rollup/plugin-commonjs/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "@rollup/plugin-commonjs/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/docs/superpowers/plans/2026-06-28-decky-vault-plugin.md b/docs/superpowers/plans/2026-06-28-decky-vault-plugin.md new file mode 100644 index 0000000..56ebd75 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-decky-vault-plugin.md @@ -0,0 +1,2528 @@ +# DeckyVault Decky Loader Plugin Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Decky Loader plugin that records Steam Deck performance metrics via MangoHud, lets users fill in manual details, and exports/uploads them to DeckyVault. + +**Architecture:** Dual-halved Decky plugin — a React/TypeScript frontend (runs in Steam's CEF context, handles UI + SteamClient events) and a Python backend (`main.py`, handles filesystem/shell/network I/O). Frontend calls backend via `callable` RPC from `@decky/api`. The plugin lives in the `plugins/decky-vault/` workspace of the Bun monorepo. + +**Tech Stack:** TypeScript + React (frontend), Python 3 (backend), `@decky/api` + `@decky/ui` + `@decky/rollup` (Decky SDK), rollup (bundler), pytest (Python tests), `@deckyvault/shared` (workspace types) + +--- + +## File Structure + +``` +plugins/decky-vault/ +├── main.py # Python backend — Plugin class with RPC methods +├── plugin.json # Decky plugin metadata (api_version, flags, publish) +├── package.json # Frontend deps (@decky/api, @decky/ui, @decky/rollup) +├── rollup.config.js # Uses @decky/rollup +├── tsconfig.json # TS config for Decky frontend +├── src/ +│ ├── index.tsx # Entry point — definePlugin, SteamClient events, tab nav +│ ├── types.d.ts # SteamClient global type declarations +│ ├── lib/ +│ │ ├── api.ts # Typed RPC wrappers (callable functions → Python backend) +│ │ └── store.ts # Frontend state management (React context + hooks) +│ └── components/ +│ ├── main-panel.tsx # Record/Stop button, session status, recent recordings +│ ├── session-form.tsx # Post-session summary (auto-captured + manual inputs + actions) +│ └── settings-panel.tsx # API key, export path, hardware override, MangoHud setup +├── tests/ +│ ├── test_mangohud_parser.py # Unit tests for log parsing +│ ├── test_settings.py # Unit tests for settings persistence +│ └── fixtures/ +│ └── sample_mangohud.log # Sample MangoHud log for parser tests +└── dist/ + └── index.js # Built frontend bundle (output of rollup) +``` + +**Backend responsibilities (main.py):** +- Settings persistence (JSON in `DECKY_PLUGIN_SETTINGS_DIR`) +- MangoHud: check install, write config, read/parse log +- System info: hardware model, OS version +- Export: write `.deckyvault.json` to disk +- Upload: HTTP POST to DeckyVault API (avoids CORS issues from CEF context) + +**Frontend responsibilities (src/):** +- UI rendering (Decky UI components) +- SteamClient game start/stop event registration +- Session state management (recording status, parsed results) +- RPC calls to Python backend +- Building the `DeckyVaultImportV1` payload from combined auto + manual data + +--- + +### Task 1: Update package.json with correct Decky dependencies + +**Files:** +- Modify: `plugins/decky-vault/package.json` + +- [ ] **Step 1: Rewrite package.json** + +Replace the entire contents with: + +```json +{ + "name": "@deckyvault/plugin", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w" + }, + "dependencies": { + "@decky/api": "^1.1.3", + "@deckyvault/shared": "workspace:*", + "react-icons": "^5.3.0", + "tslib": "^2.7.0" + }, + "devDependencies": { + "@decky/rollup": "^1.0.2", + "@decky/ui": "^4.11.6", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "rollup": "^4.53.3", + "typescript": "^5.6.2" + } +} +``` + +Note: React/react-dom are NOT listed — they are provided by Steam's CEF runtime. `@decky/rollup` handles marking them as external. + +- [ ] **Step 2: Install dependencies** + +Run: `cd plugins/decky-vault && bun install` +Expected: packages installed successfully (may need `--no-cache` if Bun has stale cache) + +- [ ] **Step 3: Commit** + +```bash +git add plugins/decky-vault/package.json +git commit -m "feat(plugin): update package.json with @decky SDK dependencies" +``` + +--- + +### Task 2: Update rollup.config.js to use @decky/rollup + +**Files:** +- Modify: `plugins/decky-vault/rollup.config.js` + +- [ ] **Step 1: Rewrite rollup.config.js** + +Replace entire contents with: + +```js +import deckyPlugin from "@decky/rollup"; + +export default deckyPlugin({ + // Add extra rollup options here if needed +}); +``` + +- [ ] **Step 2: Verify build runs (may fail on missing src, that's OK)** + +Run: `cd plugins/decky-vault && bun run build 2>&1 | head -20` +Expected: Either builds (if src/index.tsx exists) or fails on TypeScript errors (expected — we haven't rewritten the entry point yet). The important thing is that `@decky/rollup` loads without "module not found" errors. + +- [ ] **Step 3: Commit** + +```bash +git add plugins/decky-vault/rollup.config.js +git commit -m "feat(plugin): use @decky/rollup for build config" +``` + +--- + +### Task 3: Update plugin.json with correct Decky fields + +**Files:** +- Modify: `plugins/decky-vault/plugin.json` + +- [ ] **Step 1: Rewrite plugin.json** + +Replace entire contents with: + +```json +{ + "name": "DeckyVault", + "author": "DeckyVault", + "flags": ["debug"], + "api_version": 1, + "publish": { + "tags": ["performance", "benchmark", "mangohud"], + "description": "Record performance metrics and export/upload them to DeckyVault", + "image": "" + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/plugin.json +git commit -m "feat(plugin): update plugin.json with correct Decky metadata fields" +``` + +--- + +### Task 4: Update tsconfig.json for Decky frontend + +**Files:** +- Modify: `plugins/decky-vault/tsconfig.json` + +- [ ] **Step 1: Rewrite tsconfig.json** + +Replace entire contents with: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "outDir": "./dist", + "rootDir": "./src", + "declaration": false, + "sourceMap": false, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM"], + "types": ["react", "react-dom"] + }, + "include": ["src"], + "references": [ + { "path": "../../packages/shared" } + ] +} +``` + +Added `skipLibCheck` (avoids errors from @decky/ui's internal types), `lib` with DOM (needed for browser globals), and `types` for React. + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/tsconfig.json +git commit -m "feat(plugin): update tsconfig for Decky frontend build" +``` + +--- + +### Task 5: Create Python backend skeleton with settings management + +**Files:** +- Create: `plugins/decky-vault/main.py` +- Create: `plugins/decky-vault/tests/test_settings.py` + +- [ ] **Step 1: Write the test for settings persistence** + +Create `plugins/decky-vault/tests/test_settings.py`: + +```python +"""Tests for settings persistence in the Python backend.""" +import json +import os +import sys +import tempfile +import pytest + +# We test the settings logic directly, not through the Plugin class, +# so we can run tests without the decky module. + +def write_settings(settings_path, settings): + """Write settings JSON to the given path.""" + os.makedirs(os.path.dirname(settings_path), exist_ok=True) + with open(settings_path, 'w') as f: + json.dump(settings, f, indent=2) + +def read_settings(settings_path): + """Read settings JSON from the given path, return empty dict if missing.""" + if os.path.exists(settings_path): + with open(settings_path, 'r') as f: + return json.load(f) + return {} + +def test_read_settings_returns_empty_when_file_missing(): + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "settings.json") + assert read_settings(path) == {} + +def test_write_then_read_settings(): + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "subdir", "settings.json") + write_settings(path, {"apiKey": "dv_test123", "exportPath": "/home/deck/Downloads"}) + result = read_settings(path) + assert result["apiKey"] == "dv_test123" + assert result["exportPath"] == "/home/deck/Downloads" + +def test_write_settings_creates_directory(): + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "newdir", "settings.json") + write_settings(path, {"key": "value"}) + assert os.path.exists(path) + +def test_read_settings_handles_corrupt_json(): + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "settings.json") + with open(path, 'w') as f: + f.write("{invalid json") + with pytest.raises(json.JSONDecodeError): + read_settings(path) +``` + +- [ ] **Step 2: Run test to verify it fails (functions not defined in main.py yet)** + +Run: `cd plugins/decky-vault && python -m pytest tests/test_settings.py -v 2>&1 | head -20` +Expected: FAIL — the test defines its own helper functions so it should actually PASS. Wait — these are self-contained test helpers. Let me adjust: we're testing the pattern, not importing from main.py. Run it to confirm the pattern works. + +Expected: PASS (the test helpers are self-contained). This validates our settings approach before wiring it into main.py. + +- [ ] **Step 3: Create main.py with Plugin class and settings methods** + +Create `plugins/decky-vault/main.py`: + +```python +import asyncio +import json +import os + +try: + import decky +except ImportError: + # Allow running tests without the decky module (tests mock the path) + decky = None + + +class Plugin: + async def _main(self): + if decky: + decky.logger.info(f"DeckyVault plugin loaded: {decky.DECKY_PLUGIN_NAME}") + self._settings_path = self._get_settings_path() + self._settings = self._read_settings() + + async def _unload(self): + if decky: + decky.logger.info("DeckyVault plugin unloading") + + async def _uninstall(self): + if decky: + decky.logger.info("DeckyVault plugin uninstalled") + + def _get_settings_path(self): + if decky: + return os.path.join(decky.DECKY_PLUGIN_SETTINGS_DIR, "settings.json") + return os.path.join(os.path.expanduser("~"), ".deckyvault-test", "settings.json") + + def _read_settings(self): + """Read settings from JSON file. Returns empty dict if file missing.""" + if os.path.exists(self._settings_path): + try: + with open(self._settings_path, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return {} + return {} + + def _write_settings(self, settings): + """Write settings to JSON file, creating directory if needed.""" + os.makedirs(os.path.dirname(self._settings_path), exist_ok=True) + with open(self._settings_path, 'w') as f: + json.dump(settings, f, indent=2) + + async def get_settings(self) -> dict: + """RPC: Return all plugin settings.""" + return self._settings + + async def set_setting(self, key: str, value) -> dict: + """RPC: Set a single setting and persist. Returns updated settings.""" + self._settings[key] = value + self._write_settings(self._settings) + return self._settings +``` + +- [ ] **Step 4: Commit** + +```bash +git add plugins/decky-vault/main.py plugins/decky-vault/tests/test_settings.py +git commit -m "feat(plugin): add Python backend skeleton with settings management" +``` + +--- + +### Task 6: Implement MangoHud status check and config writing + +**Files:** +- Modify: `plugins/decky-vault/main.py` + +- [ ] **Step 1: Add MangoHud methods to the Plugin class** + +Add these methods to the `Plugin` class in `main.py` (after the settings methods): + +```python + async def check_mangohud(self) -> dict: + """RPC: Check if MangoHud is installed. Returns {installed: bool, path: str, version: str}.""" + import subprocess + try: + result = subprocess.run( + ["which", "mangohud"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + mangohud_path = result.stdout.strip() + # Get version + version_result = subprocess.run( + ["mangohud", "--version"], + capture_output=True, text=True, timeout=5 + ) + version = version_result.stdout.strip() if version_result.returncode == 0 else "unknown" + return {"installed": True, "path": mangohud_path, "version": version} + else: + return {"installed": False, "path": "", "version": ""} + except Exception as e: + return {"installed": False, "path": "", "version": "", "error": str(e)} + + async def write_mangohud_config(self) -> dict: + """RPC: Write the MangoHud logging config to ~/.config/MangoHud/MangoHud.conf. + Returns {success: bool, path: str, error: str?}.""" + try: + home = os.path.expanduser("~") + config_dir = os.path.join(home, ".config", "MangoHud") + config_path = os.path.join(config_dir, "MangoHud.conf") + os.makedirs(config_dir, exist_ok=True) + + # MangoHud config that enables logging with the metrics we need. + # output_folder is required for logging to work. + # We log to /tmp so the plugin can read it after the session. + config_content = """\ +# DeckyVault MangoHud logging config +output_folder=/tmp +output_file=deckyvault-mangohud.log +log_duration=0 +fps +frame_timing +cpu_power +gpu_power +cpu_temp +gpu_temp +benchmark_percentiles=97,AVG,1,0.1 +""" + with open(config_path, 'w') as f: + f.write(config_content) + + return {"success": True, "path": config_path} + except Exception as e: + return {"success": False, "path": "", "error": str(e)} + + async def get_mangohud_config(self) -> dict: + """RPC: Read the current MangoHud config. Returns {exists: bool, content: str, path: str}.""" + home = os.path.expanduser("~") + config_path = os.path.join(home, ".config", "MangoHud", "MangoHud.conf") + if os.path.exists(config_path): + with open(config_path, 'r') as f: + return {"exists": True, "content": f.read(), "path": config_path} + return {"exists": False, "content": "", "path": config_path} +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/main.py +git commit -m "feat(plugin): add MangoHud status check and config writing to backend" +``` + +--- + +### Task 7: Implement MangoHud log parser with tests + +**Files:** +- Create: `plugins/decky-vault/tests/fixtures/sample_mangohud.log` +- Create: `plugins/decky-vault/tests/test_mangohud_parser.py` +- Modify: `plugins/decky-vault/main.py` + +- [ ] **Step 1: Create a sample MangoHud log fixture** + +Create `plugins/decky-vault/tests/fixtures/sample_mangohud.log`: + +``` +# MangoHud v0.8.4 +# note: session started at 2026-06-28 14:30:00 +# preset: 0 +fps,frametime,cpu_load,gpu_load,cpu_temp,gpu_temp,gpu_power,cpu_power +60,16.67,45,80,55,65,15,10 +62,16.13,46,82,55,65,15,10 +58,17.24,44,78,56,66,14,10 +61,16.39,45,81,55,65,15,10 +59,16.95,44,79,56,66,14,10 +60,16.67,45,80,55,65,15,10 +63,15.87,47,83,55,65,15,10 +57,17.54,43,77,56,66,14,10 +60,16.67,45,80,55,65,15,10 +61,16.39,46,81,55,65,15,10 +# benchmark summary +97%, 62 +AVG, 60 +1%, 57 +0.1%, 57 +``` + +- [ ] **Step 2: Write the parser test** + +Create `plugins/decky-vault/tests/test_mangohud_parser.py`: + +```python +"""Tests for MangoHud log parsing.""" +import os + +# Parser functions — these will be imported from main.py once implemented. +# For now we define them here to test the logic, then move to main.py. + +def parse_mangohud_log(log_content: str) -> dict: + """Parse a MangoHud log file's content and return FPS stats. + + Returns: {fpsAvg, fpsLow, fpsHigh, fpsOnePercentLow, tdpWatts, error?} + """ + lines = log_content.strip().split('\n') + + # Find the header row (first non-comment, non-empty line that looks like column names) + header_idx = None + fps_col = 0 + frametime_col = None + gpu_power_col = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + # Check if this is a header (contains 'fps') + if 'fps' in stripped.lower() and ',' in stripped: + columns = [c.strip().lower() for c in stripped.split(',')] + if 'fps' in columns: + fps_col = columns.index('fps') + if 'frametime' in columns: + frametime_col = columns.index('frametime') + if 'gpu_power' in columns: + gpu_power_col = columns.index('gpu_power') + header_idx = i + break + + if header_idx is None: + return {"error": "Could not find FPS column in log header"} + + # Extract data rows (lines after header that start with a number) + fps_values = [] + frametime_values = [] + gpu_power_values = [] + + for line in lines[header_idx + 1:]: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + # Check if it's a summary line (e.g. "97%,\t62" or "AVG,\t60") + if stripped.startswith(('97%', 'AVG', '1%', '0.1%', '5%')): + continue + parts = [p.strip() for p in stripped.split(',')] + try: + fps = float(parts[fps_col]) + fps_values.append(fps) + if frametime_col is not None and frametime_col < len(parts): + ft = float(parts[frametime_col]) + frametime_values.append(ft) + if gpu_power_col is not None and gpu_power_col < len(parts): + gp = float(parts[gpu_power_col]) + gpu_power_values.append(gp) + except (ValueError, IndexError): + continue + + if not fps_values: + return {"error": "No FPS data found in log"} + + # Compute stats + fps_avg = round(sum(fps_values) / len(fps_values), 1) + fps_low = round(min(fps_values), 1) + fps_high = round(max(fps_values), 1) + + # 1% low: sort frame times, take 1st percentile, convert to FPS + if frametime_values: + sorted_ft = sorted(frametime_values) + one_percent_idx = max(0, int(len(sorted_ft) * 0.01)) + one_percent_ft = sorted_ft[one_percent_idx] + fps_one_percent_low = round(1000.0 / one_percent_ft, 1) if one_percent_ft > 0 else None + else: + # Fall back: sort FPS values, take 1st percentile from bottom + sorted_fps = sorted(fps_values) + one_percent_idx = max(0, int(len(sorted_fps) * 0.01)) + fps_one_percent_low = round(sorted_fps[one_percent_idx], 1) + + tdp_watts = None + if gpu_power_values: + tdp_watts = round(sum(gpu_power_values) / len(gpu_power_values), 1) + + return { + "fpsAvg": fps_avg, + "fpsLow": fps_low, + "fpsHigh": fps_high, + "fpsOnePercentLow": fps_one_percent_low, + "tdpWatts": tdp_watts, + } + + +# ── Tests ────────────────────────────────────────────────────────── + +def test_parse_basic_log(): + log = """\ +# MangoHud v0.8.4 +fps,frametime,cpu_load,gpu_load,cpu_temp,gpu_temp,gpu_power,cpu_power +60,16.67,45,80,55,65,15,10 +62,16.13,46,82,55,65,15,10 +58,17.24,44,78,56,66,14,10 +""" + result = parse_mangohud_log(log) + assert "error" not in result + assert result["fpsAvg"] == 60.0 + assert result["fpsLow"] == 58.0 + assert result["fpsHigh"] == 62.0 + assert result["tdpWatts"] == 14.3 # avg of 15,15,14 + + +def test_parse_log_with_summary_section(): + """The summary section (97%, AVG, 1%, 0.1%) should be skipped as data.""" + log = """\ +# MangoHud v0.8.4 +fps,frametime,cpu_load,gpu_power +60,16.67,45,15 +62,16.13,46,15 +58,17.24,44,14 +# benchmark summary +97%, 62 +AVG, 60 +1%, 57 +0.1%, 57 +""" + result = parse_mangohud_log(log) + assert "error" not in result + assert result["fpsAvg"] == 60.0 + # Should not have tried to parse summary lines as data + assert result["fpsLow"] == 58.0 + assert result["fpsHigh"] == 62.0 + + +def test_parse_empty_log_returns_error(): + result = parse_mangohud_log("") + assert "error" in result + + +def test_parse_log_without_fps_column(): + log = """\ +# no fps here +cpu_load,gpu_load +45,80 +""" + result = parse_mangohud_log(log) + assert "error" in result + + +def test_parse_log_without_gpu_power(): + """tdpWatts should be None if gpu_power column is absent.""" + log = """\ +fps,frametime,cpu_load +60,16.67,45 +62,16.13,46 +""" + result = parse_mangohud_log(log) + assert result["tdpWatts"] is None + + +def test_parse_one_percent_low_from_frametime(): + """1% low should be computed from frame times when available.""" + log = """\ +fps,frametime +60,16.67 +30,33.33 +60,16.67 +60,16.67 +60,16.67 +60,16.67 +60,16.67 +60,16.67 +60,16.67 +60,16.67 +""" + result = parse_mangohud_log(log) + # The 33.33ms frame time is the worst — 1% low should be ~30 fps + assert result["fpsOnePercentLow"] is not None + assert result["fpsOnePercentLow"] <= 35 # roughly 1000/33.33 = 30 + + +def test_parse_fixture_file(): + """Parse the actual fixture file.""" + fixture_path = os.path.join( + os.path.dirname(__file__), "fixtures", "sample_mangohud.log" + ) + with open(fixture_path, 'r') as f: + content = f.read() + result = parse_mangohud_log(content) + assert "error" not in result + assert result["fpsAvg"] == 60.1 # avg of the 10 data rows + assert result["fpsLow"] == 57.0 + assert result["fpsHigh"] == 63.0 + assert result["tdpWatts"] is not None +``` + +- [ ] **Step 3: Run the parser tests** + +Run: `cd plugins/decky-vault && python -m pytest tests/test_mangohud_parser.py -v` +Expected: All 7 tests PASS + +- [ ] **Step 4: Add the parser to main.py** + +Add this method to the `Plugin` class in `main.py` (after the MangoHud config methods). Also add a standalone `parse_mangohud_log` function at module level that the method delegates to: + +```python +def parse_mangohud_log(log_content: str) -> dict: + """Parse a MangoHud log file's content and return FPS stats. + + Returns: {fpsAvg, fpsLow, fpsHigh, fpsOnePercentLow, tdpWatts, error?} + """ + lines = log_content.strip().split('\n') + + header_idx = None + fps_col = 0 + frametime_col = None + gpu_power_col = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + if 'fps' in stripped.lower() and ',' in stripped: + columns = [c.strip().lower() for c in stripped.split(',')] + if 'fps' in columns: + fps_col = columns.index('fps') + if 'frametime' in columns: + frametime_col = columns.index('frametime') + if 'gpu_power' in columns: + gpu_power_col = columns.index('gpu_power') + header_idx = i + break + + if header_idx is None: + return {"error": "Could not find FPS column in log header"} + + fps_values = [] + frametime_values = [] + gpu_power_values = [] + + for line in lines[header_idx + 1:]: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + if stripped.startswith(('97%', 'AVG', '1%', '0.1%', '5%')): + continue + parts = [p.strip() for p in stripped.split(',')] + try: + fps = float(parts[fps_col]) + fps_values.append(fps) + if frametime_col is not None and frametime_col < len(parts): + frametime_values.append(float(parts[frametime_col])) + if gpu_power_col is not None and gpu_power_col < len(parts): + gpu_power_values.append(float(parts[gpu_power_col])) + except (ValueError, IndexError): + continue + + if not fps_values: + return {"error": "No FPS data found in log"} + + fps_avg = round(sum(fps_values) / len(fps_values), 1) + fps_low = round(min(fps_values), 1) + fps_high = round(max(fps_values), 1) + + if frametime_values: + sorted_ft = sorted(frametime_values) + one_percent_idx = max(0, int(len(sorted_ft) * 0.01)) + one_percent_ft = sorted_ft[one_percent_idx] + fps_one_percent_low = round(1000.0 / one_percent_ft, 1) if one_percent_ft > 0 else None + else: + sorted_fps = sorted(fps_values) + one_percent_idx = max(0, int(len(sorted_fps) * 0.01)) + fps_one_percent_low = round(sorted_fps[one_percent_idx], 1) + + tdp_watts = None + if gpu_power_values: + tdp_watts = round(sum(gpu_power_values) / len(gpu_power_values), 1) + + return { + "fpsAvg": fps_avg, + "fpsLow": fps_low, + "fpsHigh": fps_high, + "fpsOnePercentLow": fps_one_percent_low, + "tdpWatts": tdp_watts, + } +``` + +Then add this method to the `Plugin` class: + +```python + async def read_and_parse_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: + """RPC: Read the MangoHud log file and return parsed FPS stats. + Returns parsed stats dict or {error: str}.""" + if not os.path.exists(log_path): + return {"error": f"MangoHud log not found at {log_path}. Make sure MangoHud is enabled and logging."} + try: + with open(log_path, 'r') as f: + content = f.read() + if not content.strip(): + return {"error": "MangoHud log is empty. Recording may have been too short."} + return parse_mangohud_log(content) + except Exception as e: + return {"error": f"Failed to read log: {str(e)}"} + + async def clear_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: + """RPC: Delete the MangoHud log file so the next recording starts fresh.""" + try: + if os.path.exists(log_path): + os.remove(log_path) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} +``` + +- [ ] **Step 5: Commit** + +```bash +git add plugins/decky-vault/main.py plugins/decky-vault/tests/ +git commit -m "feat(plugin): add MangoHud log parser with unit tests" +``` + +--- + +### Task 8: Implement system info reader (Python backend) + +**Files:** +- Modify: `plugins/decky-vault/main.py` + +- [ ] **Step 1: Add system info methods to the Plugin class** + +Add these methods to `main.py` (after the MangoHud methods): + +```python + async def get_hardware_info(self) -> dict: + """RPC: Detect hardware model from DMI. Returns {slug, name, raw}.""" + # Steam Deck models: Jupiter = LCD, Galileo = OLED + product_name = "" + try: + with open("/sys/class/dmi/id/product_name", 'r') as f: + product_name = f.read().strip() + except (IOError, FileNotFoundError): + pass + + slug = "unknown" + name = "Unknown Device" + + if product_name == "Jupiter": + slug = "steamdeck-lcd" + name = "Steam Deck LCD" + elif product_name == "Galileo": + slug = "steamdeck-oled" + name = "Steam Deck OLED" + elif product_name: + name = product_name + slug = product_name.lower().replace(" ", "-") + + return {"slug": slug, "name": name, "raw": product_name} + + async def get_os_version(self) -> str: + """RPC: Read OS version from /etc/os-release.""" + try: + with open("/etc/os-release", 'r') as f: + for line in f: + if line.startswith("PRETTY_NAME="): + return line.split("=", 1)[1].strip().strip('"') + return "unknown" + except (IOError, FileNotFoundError): + return "unknown" + + async def get_proton_version(self, app_id: int) -> str: + """RPC: Attempt to read the Proton version for a Steam app. + Reads from the Steam compatdata directory.""" + try: + home = os.path.expanduser("~") + # Steam compat data lives in ~/.steam/steam/steamapps/compatdata// + compat_path = os.path.join(home, ".steam", "steam", "steamapps", "compatdata", str(app_id)) + version_file = os.path.join(compat_path, "version") + if os.path.exists(version_file): + with open(version_file, 'r') as f: + return f.read().strip() + return "" + except (IOError, FileNotFoundError): + return "" + + async def get_launch_options(self, app_id: int) -> str: + """RPC: Read launch options for a Steam app from localconfig.vdf. + This is best-effort — the VDF format is not officially documented.""" + try: + home = os.path.expanduser("~") + # localconfig.vdf path varies; try common locations + config_paths = [ + os.path.join(home, ".steam", "steam", "usercfg", "localconfig.vdf"), + os.path.join(home, ".local", "share", "Steam", "usercfg", "localconfig.vdf"), + ] + for config_path in config_paths: + if os.path.exists(config_path): + with open(config_path, 'r') as f: + content = f.read() + # Best-effort: look for LaunchOptions near the app ID + # This is a simple heuristic — VDF parsing is complex + app_str = f'"{app_id}"' + idx = content.find(app_str) + if idx != -1: + # Search for LaunchOptions within ~2000 chars after app ID + search_region = content[idx:idx + 2000] + lo_idx = search_region.find('"LaunchOptions"') + if lo_idx != -1: + # Extract the value between quotes + value_start = search_region.find('"', lo_idx + len('"LaunchOptions"')) + 1 + value_end = search_region.find('"', value_start) + if value_start > 0 and value_end > value_start: + return search_region[value_start:value_end] + return "" + return "" + except (IOError, FileNotFoundError): + return "" +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/main.py +git commit -m "feat(plugin): add system info reader (hardware, OS, Proton, launch options)" +``` + +--- + +### Task 9: Implement export to file (Python backend) + +**Files:** +- Modify: `plugins/decky-vault/main.py` + +- [ ] **Step 1: Add export method to the Plugin class** + +Add this method to `main.py` (after the system info methods): + +```python + async def export_to_file(self, data: dict, export_path: str) -> dict: + """RPC: Write a DeckyVaultImportV1 payload as JSON to the given path. + Returns {success: bool, path: str, error: str?}.""" + try: + # Sanitize the filename — the frontend passes a full path including filename + export_dir = os.path.dirname(export_path) + if export_dir and not os.path.exists(export_dir): + os.makedirs(export_dir, exist_ok=True) + + with open(export_path, 'w') as f: + json.dump(data, f, indent=2) + + return {"success": True, "path": export_path} + except PermissionError: + return {"success": False, "path": "", "error": f"Permission denied writing to {export_path}"} + except Exception as e: + return {"success": False, "path": "", "error": str(e)} +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/main.py +git commit -m "feat(plugin): add export-to-file method in backend" +``` + +--- + +### Task 10: Implement upload to DeckyVault (Python backend) + +**Files:** +- Modify: `plugins/decky-vault/main.py` + +- [ ] **Step 1: Add upload method to the Plugin class** + +Add this method to `main.py` (after the export method): + +```python + async def upload_to_deckyvault(self, data: dict, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict: + """RPC: Upload a DeckyVaultImportV1 payload to the DeckyVault API. + Uses urllib to avoid external dependencies. + Returns {success: bool, data: dict?, error: str?, status: int?}.""" + import urllib.request + import urllib.error + + try: + url = f"{base_url}/api/performance/import" + payload = json.dumps(data).encode('utf-8') + + req = urllib.request.Request( + url, + data=payload, + headers={ + "Content-Type": "application/json", + "x-api-key": api_key, + }, + method="POST" + ) + + with urllib.request.urlopen(req, timeout=30) as response: + status = response.status + body = response.read().decode('utf-8') + result = json.loads(body) + + if status == 201: + return {"success": True, "data": result, "status": status} + else: + return {"success": False, "error": result.get("error", "Upload failed"), "status": status} + + except urllib.error.HTTPError as e: + error_body = e.read().decode('utf-8') + try: + error_msg = json.loads(error_body).get("error", error_body) + except json.JSONDecodeError: + error_msg = error_body + return {"success": False, "error": error_msg, "status": e.code} + except urllib.error.URLError as e: + return {"success": False, "error": f"Network error: {str(e.reason)}", "status": 0} + except Exception as e: + return {"success": False, "error": str(e), "status": 0} + + async def test_api_key(self, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict: + """RPC: Test if an API key is valid by calling the games lookup endpoint. + Returns {valid: bool, error: str?}.""" + import urllib.request + import urllib.error + + try: + url = f"{base_url}/api/games/lookup?steamAppId=0" + req = urllib.request.Request( + url, + headers={"x-api-key": api_key}, + method="GET" + ) + with urllib.request.urlopen(req, timeout=10) as response: + # A 404 (game not found) still means the API key is valid + return {"valid": True} + except urllib.error.HTTPError as e: + if e.code == 401: + return {"valid": False, "error": "Invalid API key"} + elif e.code == 404: + return {"valid": True} # Key works, just no game with ID 0 + else: + return {"valid": False, "error": f"Server returned status {e.code}"} + except urllib.error.URLError as e: + return {"valid": False, "error": f"Network error: {str(e.reason)}"} + except Exception as e: + return {"valid": False, "error": str(e)} +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/main.py +git commit -m "feat(plugin): add upload and API key test methods in backend" +``` + +--- + +### Task 11: Update types.d.ts with correct SteamClient API + +**Files:** +- Modify: `plugins/decky-vault/src/types.d.ts` + +- [ ] **Step 1: Rewrite types.d.ts** + +Replace entire contents with: + +```typescript +// Steam Deck CEF context globals — these are Valve's internal APIs, +// available in the Steam Deck game mode browser context. +// Not part of @decky/api; accessed directly from the global scope. + +declare global { + const SteamClient: { + Apps: { + RegisterForGameStarted: ( + callback: (appId: number) => void, + ) => { unregister: () => void } + RegisterForGameStopped: ( + callback: (appId: number) => void, + ) => { unregister: () => void } + GetCurrentGameInfo: () => Promise<{ + appId: number + strAppName: string + }> + } + System: { + GetOSVersion: () => Promise + } + UI: { + GetUIMode: () => Promise + } + } +} + +export {} +``` + +Removed the non-existent `DeckyPlugin` global (we use `@decky/api` instead). Added `GetCurrentGameInfo` and `GetUIMode`. Fixed `GetAppData` to the simpler `GetCurrentGameInfo` pattern. + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/src/types.d.ts +git commit -m "feat(plugin): update SteamClient type declarations" +``` + +--- + +### Task 12: Create frontend RPC wrappers + +**Files:** +- Create: `plugins/decky-vault/src/lib/api.ts` + +- [ ] **Step 1: Write the RPC wrappers** + +Create `plugins/decky-vault/src/lib/api.ts`: + +```typescript +import { callable } from "@decky/api" + +// ── Settings ──────────────────────────────────────────────────── +export const getSettings = callable<[], Record>("get_settings") +export const setSetting = callable<[key: string, value: unknown], Record>("set_setting") + +// ── MangoHud ──────────────────────────────────────────────────── +export const checkMangohud = callable<[], { + installed: boolean + path: string + version: string + error?: string +}>("check_mangohud") + +export const writeMangohudConfig = callable<[], { + success: boolean + path: string + error?: string +}>("write_mangohud_config") + +export const getMangohudConfig = callable<[], { + exists: boolean + content: string + path: string +}>("get_mangohud_config") + +export const readAndParseMangohudLog = callable<[logPath?: string], { + fpsAvg?: number + fpsLow?: number + fpsHigh?: number + fpsOnePercentLow?: number | null + tdpWatts?: number | null + error?: string +}>("read_and_parse_mangohud_log") + +export const clearMangohudLog = callable<[logPath?: string], { + success: boolean + error?: string +}>("clear_mangohud_log") + +// ── System Info ───────────────────────────────────────────────── +export const getHardwareInfo = callable<[], { + slug: string + name: string + raw: string +}>("get_hardware_info") + +export const getOsVersion = callable<[], string>("get_os_version") + +export const getProtonVersion = callable<[appId: number], string>("get_proton_version") + +export const getLaunchOptions = callable<[appId: number], string>("get_launch_options") + +// ── Export ────────────────────────────────────────────────────── +export const exportToFile = callable<[data: Record, exportPath: string], { + success: boolean + path: string + error?: string +}>("export_to_file") + +// ── Upload ────────────────────────────────────────────────────── +export const uploadToDeckyvault = callable<[ + data: Record, + apiKey: string, + baseUrl?: string +], { + success: boolean + data?: { id: string; gameId: string; versionId: string; createdAt: string; authMethod: string } + error?: string + status?: number +}>("upload_to_deckyvault") + +export const testApiKey = callable<[apiKey: string, baseUrl?: string], { + valid: boolean + error?: string +}>("test_api_key") +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/src/lib/api.ts +git commit -m "feat(plugin): add typed RPC wrappers for Python backend" +``` + +--- + +### Task 13: Create frontend store (state management) + +**Files:** +- Create: `plugins/decky-vault/src/lib/store.ts` + +- [ ] **Step 1: Write the store module** + +Create `plugins/decky-vault/src/lib/store.ts`: + +```typescript +import { useState, useEffect, useCallback, useRef } from "react" +import type { DeckyVaultImportV1, HardwareSlug } from "@deckyvault/shared" +import { getSettings, setSetting } from "./api" + +// ── Types ─────────────────────────────────────────────────────── + +export interface PluginSettings { + apiKey: string + exportPath: string + hardwareSlug: string | null // null = auto-detect + baseUrl: string +} + +const DEFAULT_SETTINGS: PluginSettings = { + apiKey: "", + exportPath: "/home/deck/Downloads", + hardwareSlug: null, + baseUrl: "https://deckyvault.xyz", +} + +export type RecordingState = "idle" | "recording" | "stopped" + +export interface SessionData { + appId: number | null + gameName: string + startedAt: number + // Auto-captured (filled after stop) + fpsAvg: number | null + fpsLow: number | null + fpsHigh: number | null + fpsOnePercentLow: number | null + tdpWatts: number | null + hardwareSlug: string + hardwareName: string + osVersion: string + protonVersion: string + // Manual inputs (filled by user in the form) + upscalerType: string + upscalerVersion: string + frameGenMethod: string + settingsJson: string + loadTimeSsd: string + loadTimeSd: string + launchOptions: string + userNotes: string +} + +export interface RecentSession { + appId: number | null + gameName: string + fpsAvg: number | null + date: string // ISO string +} + +function createEmptySession(): SessionData { + return { + appId: null, + gameName: "", + startedAt: 0, + fpsAvg: null, + fpsLow: null, + fpsHigh: null, + fpsOnePercentLow: null, + tdpWatts: null, + hardwareSlug: "", + hardwareName: "", + osVersion: "", + protonVersion: "", + upscalerType: "none", + upscalerVersion: "", + frameGenMethod: "none", + settingsJson: "", + loadTimeSsd: "", + loadTimeSd: "", + launchOptions: "", + userNotes: "", + } +} + +// ── Settings Hook ─────────────────────────────────────────────── + +export function useSettings() { + const [settings, setSettings] = useState(DEFAULT_SETTINGS) + const [loaded, setLoaded] = useState(false) + + useEffect(() => { + async function load() { + try { + const raw = await getSettings() + setSettings({ + apiKey: (raw.apiKey as string) || "", + exportPath: (raw.exportPath as string) || DEFAULT_SETTINGS.exportPath, + hardwareSlug: (raw.hardwareSlug as string) || null, + baseUrl: (raw.baseUrl as string) || DEFAULT_SETTINGS.baseUrl, + }) + } catch (e) { + console.error("Failed to load settings:", e) + } finally { + setLoaded(true) + } + } + load() + }, []) + + const updateSetting = useCallback(async (key: keyof PluginSettings, value: string | null) => { + setSettings((prev) => ({ ...prev, [key]: value })) + try { + await setSetting(key, value) + } catch (e) { + console.error(`Failed to save setting ${key}:`, e) + } + }, []) + + return { settings, updateSetting, loaded } +} + +// ── Session Hook ──────────────────────────────────────────────── + +export function useSession() { + const [recordingState, setRecordingState] = useState("idle") + const [session, setSession] = useState(createEmptySession()) + const [recentSessions, setRecentSessions] = useState([]) + const [error, setError] = useState("") + const currentAppIdRef = useRef(null) + const currentAppNameRef = useRef("") + + const startRecording = useCallback(() => { + setError("") + setSession({ + ...createEmptySession(), + appId: currentAppIdRef.current, + gameName: currentAppNameRef.current, + startedAt: Date.now(), + }) + setRecordingState("recording") + }, []) + + const stopRecording = useCallback(() => { + setRecordingState("stopped") + }, []) + + const updateSession = useCallback((updates: Partial) => { + setSession((prev) => ({ ...prev, ...updates })) + }, []) + + const addToRecent = useCallback((sess: SessionData) => { + const recent: RecentSession = { + appId: sess.appId, + gameName: sess.gameName, + fpsAvg: sess.fpsAvg, + date: new Date().toISOString(), + } + setRecentSessions((prev) => [recent, ...prev].slice(0, 5)) + }, []) + + const reset = useCallback(() => { + setRecordingState("idle") + setSession(createEmptySession()) + setError("") + }, []) + + // Called when a game starts (via SteamClient event) + const onGameStart = useCallback((appId: number, gameName: string) => { + currentAppIdRef.current = appId + currentAppNameRef.current = gameName + }, []) + + // Called when a game stops (via SteamClient event) + const onGameStop = useCallback(() => { + currentAppIdRef.current = null + currentAppNameRef.current = "" + }, []) + + return { + recordingState, + session, + recentSessions, + error, + setError, + startRecording, + stopRecording, + updateSession, + addToRecent, + reset, + onGameStart, + onGameStop, + } +} + +// ── Payload Builder ───────────────────────────────────────────── + +export function buildImportPayload(sess: SessionData): DeckyVaultImportV1 { + return { + version: 1, + steamAppId: sess.appId ?? 0, + hardwareSlug: sess.hardwareSlug, + fpsAvg: sess.fpsAvg ?? 0, + fpsLow: sess.fpsLow, + fpsOnePercentLow: sess.fpsOnePercentLow, + fpsHigh: sess.fpsHigh, + protonVersion: sess.protonVersion || null, + osVersion: sess.osVersion || null, + upscalerType: sess.upscalerType, + upscalerVersion: sess.upscalerVersion || null, + frameGenMethod: sess.frameGenMethod, + tdpWatts: sess.tdpWatts, + loadTimeSsd: sess.loadTimeSsd ? Number(sess.loadTimeSsd) : null, + loadTimeSd: sess.loadTimeSd ? Number(sess.loadTimeSd) : null, + launchOptions: sess.launchOptions || null, + settingsJson: sess.settingsJson ? tryParseJson(sess.settingsJson) : null, + userNotes: sess.userNotes || null, + } +} + +function tryParseJson(text: string): unknown[] | null { + try { + const parsed = JSON.parse(text) + return Array.isArray(parsed) ? parsed : [parsed] + } catch { + return [{ text }] + } +} + +export { DEFAULT_SETTINGS } +export type { HardwareSlug } +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/src/lib/store.ts +git commit -m "feat(plugin): add frontend state management (settings + session hooks)" +``` + +--- + +### Task 14: Create the main panel component + +**Files:** +- Create: `plugins/decky-vault/src/components/main-panel.tsx` + +- [ ] **Step 1: Write the main panel component** + +Create `plugins/decky-vault/src/components/main-panel.tsx`: + +```tsx +import { useEffect, useState } from "react" +import { + ButtonItem, + PanelSection, + PanelSectionRow, + staticClasses, +} from "@decky/ui" +import { + FaPlay, + FaStop, + FaClock, +} from "react-icons/fa" +import type { RecordingState, SessionData, RecentSession } from "../lib/store" +import SessionForm from "./session-form" + +import type { PluginSettings } from "../lib/store" + +interface MainPanelProps { + recordingState: RecordingState + session: SessionData + recentSessions: RecentSession[] + error: string + settings: PluginSettings + onStart: () => void + onStop: () => void + onUpdateSession: (updates: Partial) => void + onAddToRecent: (sess: SessionData) => void + onReset: () => void + setError: (msg: string) => void +} + +export default function MainPanel({ + recordingState, + session, + recentSessions, + error, + settings, + onStart, + onStop, + onUpdateSession, + onAddToRecent, + onReset, + setError, +}: MainPanelProps) { + const [elapsed, setElapsed] = useState(0) + + // Timer for recording state + useEffect(() => { + if (recordingState !== "recording") { + setElapsed(0) + return + } + const interval = setInterval(() => { + setElapsed(Math.floor((Date.now() - session.startedAt) / 1000)) + }, 1000) + return () => clearInterval(interval) + }, [recordingState, session.startedAt]) + + function formatTime(seconds: number): string { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${m}:${s.toString().padStart(2, "0")}` + } + + // ── Stopped state: show the session form ────────────────────── + if (recordingState === "stopped") { + return ( + + ) + } + + // ── Idle or Recording state ─────────────────────────────────── + return ( + + {error && ( + +
+ {error} +
+
+ )} + + + {recordingState === "idle" ? ( + +
+ + Start Recording +
+
+ ) : ( + +
+ + Stop Recording +
+
+ )} +
+ + {recordingState === "recording" && ( + <> + +
+
+ + {formatTime(elapsed)} +
+
+ {session.gameName + ? `Recording: ${session.gameName}` + : "No game detected — recording anyway"} +
+
+
+ + )} + + {recordingState === "idle" && ( + +
+ Enable MangoHud for your game, then press Start Recording before launching. + Configure MangoHud in the Settings tab. +
+
+ )} + + {recentSessions.length > 0 && recordingState === "idle" && ( + + {recentSessions.map((rs, i) => ( + +
+ {rs.gameName || "Unknown game"} +
+ + {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} ·{" "} + {new Date(rs.date).toLocaleDateString()} + +
+
+ ))} +
+ )} +
+ ) +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add plugins/decky-vault/src/components/main-panel.tsx +git commit -m "feat(plugin): add main panel component (record/stop + recent sessions)" +``` + +--- + +### Task 15: Create the session form component + +**Files:** +- Create: `plugins/decky-vault/src/components/session-form.tsx` + +- [ ] **Step 1: Write the session form component** + +Create `plugins/decky-vault/src/components/session-form.tsx`: + +```tsx +import { useState } from "react" +import { + ButtonItem, + PanelSection, + PanelSectionRow, + Field, + DropdownItem, + staticClasses, +} from "@decky/ui" +import { + FaFileExport, + FaCloudUploadAlt, + FaCheck, + FaTimes, +} from "react-icons/fa" +import type { SessionData } from "../lib/store" +import { buildImportPayload } from "../lib/store" +import type { PluginSettings } from "../lib/store" +import { exportToFile, uploadToDeckyvault } from "../lib/api" + +interface SessionFormProps { + session: SessionData + error: string + settings?: PluginSettings + onUpdateSession: (updates: Partial) => void + onAddToRecent: (sess: SessionData) => void + onReset: () => void + setError: (msg: string) => void +} + +const UPSCALER_OPTIONS = [ + { label: "None", value: "none" }, + { label: "FSR", value: "fsr" }, + { label: "DLSS", value: "dlss" }, + { label: "XeSS", value: "xess" }, + { label: "LSFG", value: "lsfg" }, + { label: "Other", value: "other" }, +] + +const FRAME_GEN_OPTIONS = [ + { label: "None", value: "none" }, + { label: "FSR FG", value: "fsr_fg" }, + { label: "DLSS FG", value: "dlss_fg" }, + { label: "LSFG", value: "lsfg" }, + { label: "Other", value: "other" }, +] + +export default function SessionForm({ + session, + error, + settings, + onUpdateSession, + onAddToRecent, + onReset, + setError, +}: SessionFormProps) { + const [exportStatus, setExportStatus] = useState<"idle" | "success" | "error">("idle") + const [uploadStatus, setUploadStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [statusMessage, setStatusMessage] = useState("") + + async function handleExport() { + if (!settings) return + setError("") + setExportStatus("idle") + const payload = buildImportPayload(session) + const gameSlug = session.gameName.toLowerCase().replace(/[^a-z0-9]/g, "-") || "unknown" + const date = new Date().toISOString().slice(0, 10) + const filename = `${gameSlug}-${date}.deckyvault.json` + const fullPath = `${settings.exportPath}/${filename}` + + const result = await exportToFile(payload as unknown as Record, fullPath) + if (result.success) { + setExportStatus("success") + setStatusMessage(`Saved to ${result.path}`) + onAddToRecent(session) + } else { + setExportStatus("error") + setStatusMessage(result.error || "Export failed") + } + } + + async function handleUpload() { + if (!settings) return + if (!settings.apiKey) { + setError("No API key configured. Set one in the Settings tab.") + return + } + setError("") + setUploadStatus("loading") + setStatusMessage("") + + const payload = buildImportPayload(session) + const result = await uploadToDeckyvault( + payload as unknown as Record, + settings.apiKey, + settings.baseUrl, + ) + + if (result.success) { + setUploadStatus("success") + setStatusMessage(`Uploaded! Entry ID: ${result.data?.id}`) + onAddToRecent(session) + } else { + setUploadStatus("error") + setStatusMessage(result.error || "Upload failed") + if (result.status === 404) { + setStatusMessage("This game isn't in DeckyVault yet. Submit it on the website first, or export to file.") + } + } + } + + return ( + + {/* ── Auto-captured summary ──────────────────────────────── */} + + +
+ Game: {session.gameName || "Unknown"}
+ {session.appId && <>App ID: {session.appId}
} + FPS: {session.fpsAvg ?? "—"} avg / {session.fpsLow ?? "—"} min / {session.fpsOnePercentLow ?? "—"} 1% low / {session.fpsHigh ?? "—"} max
+ TDP: {session.tdpWatts ? `${session.tdpWatts}W` : "—"}
+ Hardware: {session.hardwareName || session.hardwareSlug || "—"}
+ OS: {session.osVersion || "—"}
+ Proton: {session.protonVersion || "—"} +
+
+
+ + {/* ── Manual inputs ──────────────────────────────────────── */} + + + onUpdateSession({ upscalerType: opt.data as string })} + /> + + + + + onUpdateSession({ upscalerVersion: e.target.value })} + placeholder="e.g. 2.4" + style={{ width: "100%", padding: "4px 8px" }} + /> + + + + + onUpdateSession({ frameGenMethod: opt.data as string })} + /> + + + + +