Merge branch 'dev' into prod
This commit is contained in:
@@ -43,3 +43,10 @@ yarn-error.log*
|
||||
|
||||
# docs/superpowers
|
||||
/docs/superpowers
|
||||
|
||||
# git worktrees
|
||||
.worktrees/
|
||||
|
||||
# stray
|
||||
post.md
|
||||
plugins/decky-vault/dist/
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[](https://deckyvault.xyz)
|
||||
[](https://nextjs.org)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](https://bun.sh)
|
||||
[](#license)
|
||||
|
||||
</div>
|
||||
@@ -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,45 +230,118 @@ 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
|
||||
├── 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
|
||||
```
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
const startPasskeyAutoFill = async () => {
|
||||
try {
|
||||
await authClient.signIn.passkey({
|
||||
autoFill: true,
|
||||
fetchOptions: {
|
||||
onSuccess: handleLoginSuccess,
|
||||
},
|
||||
}).catch((err) => {
|
||||
})
|
||||
} catch (err) {
|
||||
if (!isWebAuthnAbortError(err)) {
|
||||
console.warn("[passkey-conditional-ui]", err)
|
||||
}
|
||||
}).finally(() => {
|
||||
} finally {
|
||||
suppressPasskeyErrors = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
startPasskeyAutoFill()
|
||||
}
|
||||
return () => { mountedRef.current = false }
|
||||
}, [handleLoginSuccess])
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
# DeckyVault Decky Loader Plugin — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
A Decky Loader plugin that records Steam Deck (and other Linux handheld) performance metrics during gameplay, then exports or uploads them to DeckyVault as benchmark entries.
|
||||
|
||||
The plugin uses **MangoHud** for FPS/power logging, reads **system files** for hardware/OS/Proton detection, and lets the user fill in the few fields that can't be auto-detected (upscaler, frame gen, in-game settings, load times).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
plugins/decky-vault/src/
|
||||
├── index.tsx # Entry point — registers plugin with Decky Loader
|
||||
├── components/
|
||||
│ ├── main-panel.tsx # Record/Stop button, session status, recent recordings
|
||||
│ ├── session-form.tsx # Post-session summary with manual inputs + export/upload
|
||||
│ └── settings-panel.tsx # API key, export path, hardware, MangoHud setup
|
||||
├── lib/
|
||||
│ ├── mangohud.ts # Start/stop MangoHud logging, parse log for FPS stats
|
||||
│ ├── system-info.ts # Read hardware model, OS version, Proton version, TDP
|
||||
│ ├── api-client.ts # Upload to DeckyVault via API key
|
||||
│ ├── exporter.ts # Save .deckyvault.json to disk
|
||||
│ └── store.ts # Plugin state (settings, current session, recent sessions)
|
||||
└── types.d.ts # Decky Loader API declarations (exists)
|
||||
```
|
||||
|
||||
Each module has one clear purpose:
|
||||
|
||||
- **`mangohud.ts`** — writes MangoHud logging config, reads/parses the log file after a session. Computes FPS avg/min/max/1% low and average TDP.
|
||||
- **`system-info.ts`** — reads `/sys/class/dmi/id/product_name` for hardware detection, `SteamClient.System.GetOSVersion()` for OS, Steam app info for Proton version.
|
||||
- **`api-client.ts`** — wraps `fetch` calls to DeckyVault's `/api/performance/import` with `x-api-key` header. Also calls `/api/games/lookup` to verify the API key.
|
||||
- **`exporter.ts`** — builds a `DeckyVaultImportV1` object and writes it as JSON to the configured export path.
|
||||
- **`store.ts`** — holds plugin settings (API key, export path, hardware override) and session state (recording status, current app ID, start time, parsed results). Also persists the last 5 recent sessions to Decky Loader's plugin storage so they survive plugin reloads. The single source of truth that components read from.
|
||||
|
||||
## Recording Flow
|
||||
|
||||
### Start Recording
|
||||
|
||||
1. User presses "Start Recording" in the plugin panel.
|
||||
2. Plugin writes a MangoHud config that enables logging. The config sets `output_folder=/tmp`, `output_file=deckyvault-mangohud.log`, and enables: `fps`, `frame_timing`, `cpu_power`, `gpu_power`, `cpu_temp`, `gpu_temp`. MangoHud's log includes a summary section with benchmark percentiles (configurable via `benchmark_percentiles`, default `97,AVG,1,0.1`). The plugin can parse either the raw frame data or the summary section for FPS stats.
|
||||
3. Plugin records the current timestamp and (if a game is running) the active app ID via `SteamClient.Apps`.
|
||||
4. Plugin checks if MangoHud is running. If not, shows a warning guiding the user to enable MangoHud for their game (via Steam launch options `mangohud %command%` or the Decky MangoHud toggle). Recording continues regardless — the log file will be populated once MangoHud is active.
|
||||
|
||||
### During Recording
|
||||
|
||||
- The main panel shows a live status: "Recording — [game name]" with an elapsed timer.
|
||||
- The MangoHud log file accumulates FPS and power samples in the background.
|
||||
- User plays their game normally.
|
||||
|
||||
### Stop Recording
|
||||
|
||||
1. User presses "Stop Recording".
|
||||
2. Plugin reads the MangoHud log file at `/tmp/deckyvault-mangohud.log` and computes:
|
||||
- `fpsAvg` — mean of all FPS samples
|
||||
- `fpsLow` — minimum FPS
|
||||
- `fpsHigh` — maximum FPS
|
||||
- `fpsOnePercentLow` — 1st percentile of frame times, converted to FPS
|
||||
- `tdpWatts` — average power draw across samples (if logged)
|
||||
3. Plugin reads system info:
|
||||
- `hardwareSlug` — from `/sys/class/dmi/id/product_name` ("Jupiter" → `steamdeck-lcd`, "Galileo" → `steamdeck-oled`; falls back to user-configured default)
|
||||
- `osVersion` — `SteamClient.System.GetOSVersion()`
|
||||
- `protonVersion` — from the Steam app info or process environment
|
||||
4. Plugin transitions to the session summary form.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- **No MangoHud log found** → error: "MangoHud logging not detected. Make sure MangoHud is enabled for this game." with a link to the Settings → MangoHud Setup section.
|
||||
- **Log file is empty** → error: "Recording was too short or MangoHud didn't capture data. Try again."
|
||||
- **No game running when Record pressed** → allowed, but shows a warning that game info won't be auto-filled. User can still manually enter the Steam App ID in the form.
|
||||
- **MangoHud log parsing fails** (corrupt/unexpected format) → error with raw log preview, fall back to manual FPS entry in the form.
|
||||
|
||||
## Session Summary Form
|
||||
|
||||
After stopping, the plugin shows a form with two sections:
|
||||
|
||||
### Auto-Captured (read-only summary card)
|
||||
|
||||
- Game name + Steam App ID
|
||||
- FPS: avg / min / 1% low / max
|
||||
- TDP (watts, average)
|
||||
- Hardware (e.g. "Steam Deck OLED")
|
||||
- OS version
|
||||
- Proton version
|
||||
|
||||
### Manual Inputs (user fills in)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| Upscaler type | dropdown | yes (default: None) | None / FSR / DLSS / XeSS / LSFG / Other |
|
||||
| Upscaler version | text | no | e.g. "2.4" |
|
||||
| Frame gen method | dropdown | yes (default: None) | None / FSR FG / DLSS FG / LSFG / Other |
|
||||
| In-game settings | free text (stored as JSON array) | no | preset, graphics quality, resolution, etc. Maps to `settingsJson` in the import format. |
|
||||
| Load time (SSD) | number (seconds) | no | |
|
||||
| Load time (SD card) | number (seconds) | no | |
|
||||
| Launch options | text | no | auto-filled from Steam if available |
|
||||
| User notes | textarea | no | max 5000 chars |
|
||||
|
||||
### Export / Upload Actions
|
||||
|
||||
Two buttons at the bottom of the form:
|
||||
|
||||
- **Export to File** — builds a `DeckyVaultImportV1` payload and saves it as `[game-name]-[date].deckyvault.json` to the configured export path.
|
||||
- **Upload to DeckyVault** — builds the same payload and POSTs to `/api/performance/import` with the `x-api-key` header. Shows success or error response.
|
||||
|
||||
Both build the identical `DeckyVaultImportV1` payload. If upload fails (invalid API key, game not in DeckyVault DB, network error), the error message is shown inline and the user can retry or fall back to file export.
|
||||
|
||||
## Plugin UI Layout
|
||||
|
||||
Two tabs in the Decky Loader Quick Access panel:
|
||||
|
||||
### Tab 1: DeckyVault (Main)
|
||||
|
||||
**Idle state:**
|
||||
- Big "Start Recording" button
|
||||
- Brief instructions: "Enable MangoHud for your game, then press Record before launching."
|
||||
|
||||
**Recording state:**
|
||||
- "Stop Recording" button
|
||||
- Live elapsed timer
|
||||
- Current game name (or "No game detected" if none running)
|
||||
|
||||
**Stopped state:**
|
||||
- The session summary form (above) replaces the button area.
|
||||
|
||||
**Below (always visible):**
|
||||
- "Recent recordings" list — last 5 sessions showing game name, FPS avg, date. Clickable to re-view the form and re-export/re-upload.
|
||||
|
||||
### Tab 2: Settings
|
||||
|
||||
- **API Key** — text input (prefixed `dv_`), with a "Test Key" button that calls `GET /api/games/lookup?steamAppId=0` to verify the key works. Shows ✓ valid or ✗ invalid.
|
||||
- **Export Path** — text input, defaults to `/home/deck/Downloads`. Where `.deckyvault.json` files are saved.
|
||||
- **Default Hardware** — auto-detected but overrideable dropdown. Options from `KNOWN_HARDWARE_SLUGS`. Useful for non-standard setups.
|
||||
- **MangoHud Setup** — see below.
|
||||
|
||||
### MangoHud Setup Section
|
||||
|
||||
- **"Check MangoHud status" button** — runs `which mangohud` and reports: installed/not installed + version if available.
|
||||
|
||||
- **Installation guide** (collapsible):
|
||||
- **Steam Deck (SteamOS)**: MangoHud is pre-installed. Enable per-game via Steam launch options (`mangohud %command%`) or the Decky MangoHud toggle plugin.
|
||||
- **Other Linux handhelds** (ROG Ally, Legion Go, etc.): install via Flatpak (`flatpak install flathub org.freedesktop.Platform.VulkanLayer.MangoHud`) or system package manager.
|
||||
- Link to MangoHud GitHub (`https://github.com/flightlessmango/MangoHud`) for manual builds.
|
||||
|
||||
- **Configuration guide** — shows the exact MangoHud config the plugin writes (so users can verify). Includes a "Write config now" button that writes the logging config to `~/.config/MangoHud/MangoHud.conf`.
|
||||
|
||||
- **Troubleshooting** (collapsible):
|
||||
- Log file empty → check MangoHud is enabled for the game, check the log path.
|
||||
- Wrong path → ensure the plugin has write access to `/tmp/`.
|
||||
- MangoHud not attaching → try adding `mangohud %command%` to the game's Steam launch options explicitly.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
[Start Recording]
|
||||
│
|
||||
▼
|
||||
[MangoHud logs to /tmp/deckyvault-mangohud.log]
|
||||
│
|
||||
▼ (user plays game)
|
||||
[Stop Recording]
|
||||
│
|
||||
├──► mangohud.ts parses log → FPS stats, TDP
|
||||
├──► system-info.ts reads → hardware, OS, Proton
|
||||
│
|
||||
▼
|
||||
[Session Summary Form]
|
||||
│ (user fills manual fields)
|
||||
├──► exporter.ts → .deckyvault.json on disk
|
||||
└──► api-client.ts → POST /api/performance/import
|
||||
│
|
||||
▼
|
||||
[DeckyVault database]
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **MangoHud not installed** → Settings tab shows installation guide. Main tab warns when Record is pressed.
|
||||
- **MangoHud not enabled for game** → post-session error with link to Settings → MangoHud Setup.
|
||||
- **Invalid API key** → upload fails with 401, message shown in form. User directed to Settings to re-enter key.
|
||||
- **Game not in DeckyVault DB** → upload returns 404, message: "This game isn't in DeckyVault yet. Submit it on the website first, or export to file for now."
|
||||
- **Network error during upload** → message shown, user can retry or export to file.
|
||||
- **Filesystem write error (export)** → message: "Couldn't write to [path]. Check the path in Settings."
|
||||
|
||||
## Testing
|
||||
|
||||
Since the plugin runs on Steam Deck hardware in the Decky Loader environment, testing is primarily manual:
|
||||
|
||||
1. **MangoHud log parsing** — unit-testable with sample log files. Create test fixtures of MangoHud CSV output and verify FPS computation.
|
||||
2. **System info reading** — testable on any Linux machine with mock `/sys` files.
|
||||
3. **API client** — testable with mock fetch responses (success, 401, 404, network error).
|
||||
4. **Exporter** — testable by writing to a temp directory and verifying JSON structure matches `DeckyVaultImportV1`.
|
||||
5. **End-to-end** — manual testing on a Steam Deck with MangoHud enabled, recording a game session and verifying upload/export.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@deckyvault/shared` — workspace package (types: `DeckyVaultImportV1`, `KNOWN_HARDWARE_SLUGS`)
|
||||
- React — provided by Decky Loader runtime
|
||||
- MangoHud — external dependency, must be installed by the user (pre-installed on Steam Deck)
|
||||
|
||||
## Out of Scope (for this phase)
|
||||
|
||||
- Auto-detection of upscaler / frame gen / in-game settings (manual input only)
|
||||
- Automatic recording on game start (manual start/stop only)
|
||||
- Real-time performance overlay in the plugin (MangoHud already provides this)
|
||||
- Multi-session batch upload (one session at a time)
|
||||
- Support for non-Steam games (requires manual Steam App ID entry, which is handled in the form)
|
||||
@@ -20,5 +20,8 @@
|
||||
"overrides": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^1.6.22"
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"devDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
@@ -0,0 +1,416 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
|
||||
def _get_ssl_context():
|
||||
"""Create an SSL context, trying verification first, falling back to unverified.
|
||||
This handles systems where the CA bundle is missing or outdated (e.g., Steam Deck)."""
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
# Test that the context can actually verify by checking it has CAs
|
||||
if ctx.get_ca_certs():
|
||||
return ctx
|
||||
except Exception:
|
||||
pass
|
||||
# Fall back to unverified if default context fails
|
||||
return ssl._create_unverified_context()
|
||||
|
||||
try:
|
||||
import decky
|
||||
except ImportError:
|
||||
# Allow running tests without the decky module (tests mock the path)
|
||||
decky = None
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 1% low: average the slowest 1% of frame times (largest frametimes),
|
||||
# then convert to FPS. Falls back to the lowest FPS percentile if no
|
||||
# frametime data is available.
|
||||
if frametime_values:
|
||||
sorted_ft = sorted(frametime_values)
|
||||
one_percent_count = max(1, int(len(sorted_ft) * 0.01))
|
||||
worst_ft = sorted_ft[-one_percent_count:]
|
||||
avg_worst_ft = sum(worst_ft) / len(worst_ft)
|
||||
fps_one_percent_low = round(1000.0 / avg_worst_ft, 1) if avg_worst_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,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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}
|
||||
|
||||
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)}
|
||||
|
||||
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/<appid>/
|
||||
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 ""
|
||||
|
||||
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)}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
context = _get_ssl_context()
|
||||
with urllib.request.urlopen(req, timeout=30, context=context) 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"
|
||||
)
|
||||
context = _get_ssl_context()
|
||||
with urllib.request.urlopen(req, timeout=10, context=context) 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 in (400, 404):
|
||||
return {"valid": True} # Key works, just bad request or 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)}
|
||||
@@ -5,15 +5,20 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w"
|
||||
"watch": "rollup -c -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deckyvault/shared": "workspace:*"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "DeckyVault",
|
||||
"version": "0.1.0",
|
||||
"description": "Automatically record performance metrics and export/upload to DeckyVault",
|
||||
"author": "DeckyVault",
|
||||
"license": "MIT",
|
||||
"icon": "",
|
||||
"permissions": ["system"]
|
||||
"flags": ["debug"],
|
||||
"api_version": 1,
|
||||
"publish": {
|
||||
"tags": ["performance", "benchmark", "mangohud"],
|
||||
"description": "Record performance metrics and export/upload them to DeckyVault",
|
||||
"image": ""
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,5 @@
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
import commonjs from "@rollup/plugin-commonjs"
|
||||
import deckyPlugin from "@decky/rollup";
|
||||
|
||||
export default {
|
||||
input: "src/index.tsx",
|
||||
output: {
|
||||
file: "dist/index.js",
|
||||
format: "esm",
|
||||
sourcemap: false,
|
||||
},
|
||||
plugins: [
|
||||
typescript(),
|
||||
commonjs(),
|
||||
],
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"@deckyvault/shared",
|
||||
],
|
||||
}
|
||||
export default deckyPlugin({
|
||||
// Add extra rollup options here if needed
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
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, PluginSettings } from "../lib/store"
|
||||
import SessionForm from "./session-form"
|
||||
|
||||
interface MainPanelProps {
|
||||
recordingState: RecordingState
|
||||
session: SessionData
|
||||
recentSessions: RecentSession[]
|
||||
error: string
|
||||
settings: PluginSettings
|
||||
onStart: () => void
|
||||
onStop: () => void
|
||||
onUpdateSession: (updates: Partial<SessionData>) => 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 (
|
||||
<SessionForm
|
||||
session={session}
|
||||
error={error}
|
||||
settings={settings}
|
||||
onUpdateSession={onUpdateSession}
|
||||
onAddToRecent={onAddToRecent}
|
||||
onReset={onReset}
|
||||
setError={setError}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Idle or Recording state ───────────────────────────────────
|
||||
return (
|
||||
<PanelSection title="Recording">
|
||||
{error && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ color: "#e74c3c", padding: "8px" }}>
|
||||
{error}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
<PanelSectionRow>
|
||||
{recordingState === "idle" ? (
|
||||
<ButtonItem layout="below" onClick={onStart}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaPlay />
|
||||
Start Recording
|
||||
</div>
|
||||
</ButtonItem>
|
||||
) : (
|
||||
<ButtonItem layout="below" onClick={onStop} disabled={false}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaStop />
|
||||
Stop Recording
|
||||
</div>
|
||||
</ButtonItem>
|
||||
)}
|
||||
</PanelSectionRow>
|
||||
|
||||
{recordingState === "recording" && (
|
||||
<>
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ padding: "8px 0" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "4px" }}>
|
||||
<FaClock />
|
||||
<strong>{formatTime(elapsed)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{session.gameName
|
||||
? `Recording: ${session.gameName}`
|
||||
: "No game detected — recording anyway"}
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</>
|
||||
)}
|
||||
|
||||
{recordingState === "idle" && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ padding: "8px 0", fontSize: "12px", opacity: 0.7 }}>
|
||||
Enable MangoHud for your game, then press Start Recording before launching.
|
||||
Configure MangoHud in the Settings tab.
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
{recentSessions.length > 0 && recordingState === "idle" && (
|
||||
<PanelSection title="Recent Recordings">
|
||||
{recentSessions.map((rs, i) => (
|
||||
<PanelSectionRow key={i}>
|
||||
<div className={staticClasses.Text} style={{ padding: "4px 0", fontSize: "13px" }}>
|
||||
<strong>{rs.gameName || "Unknown game"}</strong>
|
||||
<br />
|
||||
<span style={{ opacity: 0.6 }}>
|
||||
{rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} ·{" "}
|
||||
{new Date(rs.date).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
</PanelSection>
|
||||
)}
|
||||
</PanelSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ButtonItem,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
DropdownItem,
|
||||
TextField,
|
||||
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<SessionData>) => 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<string, unknown>, 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<string, unknown>,
|
||||
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 (
|
||||
<PanelSection title="Session Results">
|
||||
{/* ── Auto-captured summary ──────────────────────────────── */}
|
||||
<PanelSection title="Captured Metrics">
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ fontSize: "13px", padding: "4px 0" }}>
|
||||
<strong>Game:</strong> {session.gameName || "Unknown"}<br />
|
||||
{session.appId && <><strong>App ID:</strong> {session.appId}<br /></>}
|
||||
<strong>FPS:</strong> {session.fpsAvg ?? "—"} avg / {session.fpsLow ?? "—"} min / {session.fpsOnePercentLow ?? "—"} 1% low / {session.fpsHigh ?? "—"} max<br />
|
||||
<strong>TDP:</strong> {session.tdpWatts ? `${session.tdpWatts}W` : "—"}<br />
|
||||
<strong>Hardware:</strong> {session.hardwareName || session.hardwareSlug || "—"}<br />
|
||||
<strong>OS:</strong> {session.osVersion || "—"}<br />
|
||||
<strong>Proton:</strong> {session.protonVersion || "—"}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
|
||||
{/* ── Manual inputs ──────────────────────────────────────── */}
|
||||
<PanelSection title="Additional Details">
|
||||
<PanelSectionRow>
|
||||
<DropdownItem
|
||||
label="Upscaler"
|
||||
rgOptions={UPSCALER_OPTIONS}
|
||||
selectedOption={session.upscalerType}
|
||||
onChange={(opt) => onUpdateSession({ upscalerType: opt.data as string })}
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Upscaler Version"
|
||||
value={session.upscalerVersion}
|
||||
onChange={(e) => onUpdateSession({ upscalerVersion: e.target.value })}
|
||||
placeholder="e.g. 2.4"
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<DropdownItem
|
||||
label="Frame Generation"
|
||||
rgOptions={FRAME_GEN_OPTIONS}
|
||||
selectedOption={session.frameGenMethod}
|
||||
onChange={(opt) => onUpdateSession({ frameGenMethod: opt.data as string })}
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<Field label="In-game Settings" bottomSeparator="none">
|
||||
<textarea
|
||||
value={session.settingsJson}
|
||||
onChange={(e) => onUpdateSession({ settingsJson: e.target.value })}
|
||||
placeholder="e.g. High preset, 1280x800, TAA"
|
||||
rows={3}
|
||||
style={{ width: "100%", padding: "4px 8px", resize: "vertical" }}
|
||||
/>
|
||||
</Field>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Load Time - SSD (seconds)"
|
||||
value={session.loadTimeSsd}
|
||||
onChange={(e) => onUpdateSession({ loadTimeSsd: e.target.value })}
|
||||
placeholder="e.g. 12.5"
|
||||
mustBeNumeric
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Load Time - SD Card (seconds)"
|
||||
value={session.loadTimeSd}
|
||||
onChange={(e) => onUpdateSession({ loadTimeSd: e.target.value })}
|
||||
placeholder="e.g. 25.0"
|
||||
mustBeNumeric
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Launch Options"
|
||||
value={session.launchOptions}
|
||||
onChange={(e) => onUpdateSession({ launchOptions: e.target.value })}
|
||||
placeholder="e.g. mangohud %command%"
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<Field label="Notes" bottomSeparator="none">
|
||||
<textarea
|
||||
value={session.userNotes}
|
||||
onChange={(e) => onUpdateSession({ userNotes: e.target.value })}
|
||||
placeholder="Any observations about performance..."
|
||||
rows={3}
|
||||
maxLength={5000}
|
||||
style={{ width: "100%", padding: "4px 8px", resize: "vertical" }}
|
||||
/>
|
||||
</Field>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
|
||||
{/* ── Error display ──────────────────────────────────────── */}
|
||||
{error && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ color: "#e74c3c", padding: "8px" }}>
|
||||
{error}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
{/* ── Status messages ────────────────────────────────────── */}
|
||||
{statusMessage && (
|
||||
<PanelSectionRow>
|
||||
<div
|
||||
className={staticClasses.Text}
|
||||
style={{
|
||||
padding: "8px",
|
||||
color: exportStatus === "success" || uploadStatus === "success" ? "#2ecc71" : "#e74c3c",
|
||||
}}
|
||||
>
|
||||
{exportStatus === "success" && <FaCheck />}{" "}
|
||||
{exportStatus === "error" && <FaTimes />}{" "}
|
||||
{uploadStatus === "success" && <FaCheck />}{" "}
|
||||
{uploadStatus === "error" && <FaTimes />}{" "}
|
||||
{statusMessage}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
{/* ── Action buttons ─────────────────────────────────────── */}
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={handleExport} disabled={false}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaFileExport />
|
||||
Export to File
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<ButtonItem
|
||||
layout="below"
|
||||
onClick={handleUpload}
|
||||
disabled={uploadStatus === "loading"}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaCloudUploadAlt />
|
||||
{uploadStatus === "loading" ? "Uploading..." : "Upload to DeckyVault"}
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={onReset} disabled={false}>
|
||||
New Recording
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ButtonItem,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
DropdownItem,
|
||||
TextField,
|
||||
staticClasses,
|
||||
} from "@decky/ui"
|
||||
import {
|
||||
FaCheck,
|
||||
FaTimes,
|
||||
FaDownload,
|
||||
FaCog,
|
||||
} from "react-icons/fa"
|
||||
import type { PluginSettings } from "../lib/store"
|
||||
import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared"
|
||||
import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig } from "../lib/api"
|
||||
|
||||
interface SettingsPanelProps {
|
||||
settings: PluginSettings
|
||||
onUpdateSetting: <K extends keyof PluginSettings>(
|
||||
key: K,
|
||||
value: string | null
|
||||
) => void
|
||||
}
|
||||
|
||||
const HARDWARE_OPTIONS = [
|
||||
{ label: "Auto-detect", value: "" },
|
||||
...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, value: slug })),
|
||||
]
|
||||
|
||||
export default function SettingsPanel({
|
||||
settings,
|
||||
onUpdateSetting,
|
||||
}: SettingsPanelProps) {
|
||||
const [keyTestStatus, setKeyTestStatus] = useState<"idle" | "testing" | "valid" | "invalid">("idle")
|
||||
const [keyTestMessage, setKeyTestMessage] = useState("")
|
||||
const [mangohudStatus, setMangohudStatus] = useState<{
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
path: string
|
||||
version: string
|
||||
}>({ checked: false, installed: false, path: "", version: "" })
|
||||
const [showMangohudGuide, setShowMangohudGuide] = useState(false)
|
||||
const [configWritten, setConfigWritten] = useState(false)
|
||||
|
||||
async function handleTestKey() {
|
||||
if (!settings.apiKey) {
|
||||
setKeyTestStatus("invalid")
|
||||
setKeyTestMessage("Enter an API key first")
|
||||
return
|
||||
}
|
||||
setKeyTestStatus("testing")
|
||||
setKeyTestMessage("")
|
||||
const result = await testApiKey(settings.apiKey, settings.baseUrl)
|
||||
if (result.valid) {
|
||||
setKeyTestStatus("valid")
|
||||
setKeyTestMessage("API key is valid")
|
||||
} else {
|
||||
setKeyTestStatus("invalid")
|
||||
setKeyTestMessage(result.error || "Invalid API key")
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCheckMangohud() {
|
||||
const result = await checkMangohud()
|
||||
setMangohudStatus({
|
||||
checked: true,
|
||||
installed: result.installed,
|
||||
path: result.path,
|
||||
version: result.version,
|
||||
})
|
||||
}
|
||||
|
||||
async function handleWriteConfig() {
|
||||
const result = await writeMangohudConfig()
|
||||
setConfigWritten(result.success)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── API Key ─────────────────────────────────────────────── */}
|
||||
<PanelSection title="DeckyVault Account">
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="API Key"
|
||||
value={settings.apiKey}
|
||||
onChange={(e) => onUpdateSetting("apiKey", e.target.value)}
|
||||
placeholder="dv_..."
|
||||
bIsPassword
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={handleTestKey} disabled={keyTestStatus === "testing"}>
|
||||
{keyTestStatus === "testing" ? "Testing..." : "Test Key"}
|
||||
{keyTestStatus === "valid" && <FaCheck style={{ color: "#2ecc71", marginLeft: "8px" }} />}
|
||||
{keyTestStatus === "invalid" && <FaTimes style={{ color: "#e74c3c", marginLeft: "8px" }} />}
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
{keyTestMessage && (
|
||||
<PanelSectionRow>
|
||||
<div
|
||||
className={staticClasses.Text}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: keyTestStatus === "valid" ? "#2ecc71" : "#e74c3c",
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
{keyTestMessage}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ fontSize: "11px", opacity: 0.6, padding: "4px 0" }}>
|
||||
Get your API key from DeckyVault → Profile → Settings → API Keys
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
|
||||
{/* ── Export Path ─────────────────────────────────────────── */}
|
||||
<PanelSection title="Export">
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Export Path"
|
||||
value={settings.exportPath}
|
||||
onChange={(e) => onUpdateSetting("exportPath", e.target.value)}
|
||||
placeholder="/home/deck/Downloads"
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<TextField
|
||||
label="Server URL"
|
||||
value={settings.baseUrl}
|
||||
onChange={(e) => onUpdateSetting("baseUrl", e.target.value)}
|
||||
placeholder="https://deckyvault.xyz"
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
|
||||
<PanelSectionRow>
|
||||
<DropdownItem
|
||||
label="Default Hardware"
|
||||
rgOptions={HARDWARE_OPTIONS}
|
||||
selectedOption={settings.hardwareSlug || ""}
|
||||
onChange={(opt) => onUpdateSetting("hardwareSlug", opt.data as string || null)}
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
|
||||
{/* ── MangoHud Setup ──────────────────────────────────────── */}
|
||||
<PanelSection title="MangoHud Setup">
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={handleCheckMangohud}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaCog />
|
||||
Check MangoHud Status
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
{mangohudStatus.checked && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ fontSize: "13px", padding: "8px 0" }}>
|
||||
{mangohudStatus.installed ? (
|
||||
<>
|
||||
<FaCheck style={{ color: "#2ecc71" }} /> MangoHud installed
|
||||
<br />
|
||||
<span style={{ opacity: 0.7 }}>
|
||||
Path: {mangohudStatus.path}
|
||||
<br />
|
||||
Version: {mangohudStatus.version}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaTimes style={{ color: "#e74c3c" }} /> MangoHud not found
|
||||
<br />
|
||||
<span style={{ opacity: 0.7 }}>See installation guide below</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={handleWriteConfig}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FaDownload />
|
||||
Write MangoHud Config
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
{configWritten && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ fontSize: "12px", color: "#2ecc71", padding: "4px 0" }}>
|
||||
<FaCheck /> Config written to ~/.config/MangoHud/MangoHud.conf
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" onClick={() => setShowMangohudGuide(!showMangohudGuide)}>
|
||||
{showMangohudGuide ? "Hide Guide" : "Show Installation Guide"}
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
|
||||
{showMangohudGuide && (
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ fontSize: "12px", padding: "8px", lineHeight: "1.6" }}>
|
||||
<strong>Steam Deck (SteamOS):</strong>
|
||||
<br />
|
||||
MangoHud is pre-installed. Enable it per-game by adding
|
||||
<code style={{ display: "block", margin: "4px 0", padding: "4px", background: "rgba(255,255,255,0.1)" }}>
|
||||
mangohud %command%
|
||||
</code>
|
||||
to the game's Steam launch options (right-click game → Properties → Launch Options).
|
||||
|
||||
<br /><br />
|
||||
<strong>Other Linux handhelds</strong> (ROG Ally, Legion Go):
|
||||
<br />
|
||||
Install via package manager:
|
||||
<code style={{ display: "block", margin: "4px 0", padding: "4px", background: "rgba(255,255,255,0.1)" }}>
|
||||
sudo apt install mangohud
|
||||
</code>
|
||||
or Flatpak:
|
||||
<code style={{ display: "block", margin: "4px 0", padding: "4px", background: "rgba(255,255,255,0.1)" }}>
|
||||
flatpak install flathub org.freedesktop.Platform.VulkanLayer.MangoHud
|
||||
</code>
|
||||
|
||||
<br /><br />
|
||||
<strong>Manual build:</strong>
|
||||
<br />
|
||||
See{" "}
|
||||
<a href="https://github.com/flightlessmango/MangoHud" style={{ color: "#66c0f4" }}>
|
||||
github.com/flightlessmango/MangoHud
|
||||
</a>
|
||||
|
||||
<br /><br />
|
||||
<strong>Troubleshooting:</strong>
|
||||
<br />
|
||||
• Log file empty? Check MangoHud is enabled for the game and the config was written.
|
||||
<br />
|
||||
• Wrong path? Ensure the plugin can write to /tmp/.
|
||||
<br />
|
||||
• Not attaching? Try adding <code>mangohud %command%</code> to Steam launch options explicitly.
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
</PanelSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,177 @@
|
||||
import { DeckyVaultImportV1 } from "@deckyvault/shared"
|
||||
import { useEffect, useRef } from "react"
|
||||
import {
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
staticClasses,
|
||||
} from "@decky/ui"
|
||||
import {
|
||||
definePlugin,
|
||||
} from "@decky/api"
|
||||
import { FaDatabase } from "react-icons/fa"
|
||||
import MainPanel from "./components/main-panel"
|
||||
import SettingsPanel from "./components/settings-panel"
|
||||
import { useSettings, useSession } from "./lib/store"
|
||||
import {
|
||||
readAndParseMangohudLog,
|
||||
clearMangohudLog,
|
||||
getHardwareInfo,
|
||||
getOsVersion,
|
||||
getProtonVersion,
|
||||
getLaunchOptions,
|
||||
} from "./lib/api"
|
||||
|
||||
interface PluginSettings {
|
||||
apiKey: string
|
||||
autoRecord: boolean
|
||||
exportPath: string
|
||||
function Content() {
|
||||
const { settings, updateSetting, loaded } = useSettings()
|
||||
const {
|
||||
recordingState,
|
||||
session,
|
||||
recentSessions,
|
||||
error,
|
||||
setError,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
updateSession,
|
||||
addToRecent,
|
||||
reset,
|
||||
onGameStart,
|
||||
onGameStop,
|
||||
} = useSession()
|
||||
const gameStartedUnregRef = useRef<{ unregister: () => void } | null>(null)
|
||||
const gameStoppedUnregRef = useRef<{ unregister: () => void } | null>(null)
|
||||
|
||||
// ── Register SteamClient game events ──────────────────────────
|
||||
useEffect(() => {
|
||||
try {
|
||||
const startedReg = SteamClient.Apps.RegisterForGameStarted(async (appId: number) => {
|
||||
let gameName = `App ${appId}`
|
||||
try {
|
||||
const info = await SteamClient.Apps.GetCurrentGameInfo()
|
||||
if (info.appId === appId) {
|
||||
gameName = info.strAppName
|
||||
}
|
||||
} catch {
|
||||
// GetCurrentGameInfo may not be available in all contexts
|
||||
}
|
||||
onGameStart(appId, gameName)
|
||||
})
|
||||
gameStartedUnregRef.current = startedReg
|
||||
|
||||
const stoppedReg = SteamClient.Apps.RegisterForGameStopped((_appId: number) => {
|
||||
onGameStop()
|
||||
})
|
||||
gameStoppedUnregRef.current = stoppedReg
|
||||
} catch (e) {
|
||||
console.warn("[DeckyVault] SteamClient event registration failed:", e)
|
||||
}
|
||||
|
||||
let settings: PluginSettings = {
|
||||
apiKey: "",
|
||||
autoRecord: false,
|
||||
exportPath: "/home/deck/Downloads",
|
||||
return () => {
|
||||
try {
|
||||
gameStartedUnregRef.current?.unregister()
|
||||
gameStoppedUnregRef.current?.unregister()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [onGameStart, onGameStop])
|
||||
|
||||
// ── Handle start recording ────────────────────────────────────
|
||||
async function handleStart() {
|
||||
// Clear any previous log file
|
||||
await clearMangohudLog()
|
||||
startRecording()
|
||||
}
|
||||
|
||||
export default {
|
||||
// ── Handle stop recording: parse log + read system info ────────
|
||||
async function handleStop() {
|
||||
stopRecording()
|
||||
|
||||
// Parse the MangoHud log
|
||||
const logResult = await readAndParseMangohudLog()
|
||||
if (logResult.error) {
|
||||
setError(logResult.error)
|
||||
// Still transition to stopped state so user can see the error + manual fields
|
||||
return
|
||||
}
|
||||
|
||||
// Read system info in parallel
|
||||
const [hwInfo, osVersion] = await Promise.all([
|
||||
getHardwareInfo(),
|
||||
getOsVersion(),
|
||||
])
|
||||
|
||||
// Read Proton version + launch options if we have an app ID
|
||||
let protonVersion = ""
|
||||
let launchOptions = ""
|
||||
if (session.appId) {
|
||||
const [pv, lo] = await Promise.all([
|
||||
getProtonVersion(session.appId),
|
||||
getLaunchOptions(session.appId),
|
||||
])
|
||||
protonVersion = pv
|
||||
launchOptions = lo
|
||||
}
|
||||
|
||||
// Use settings hardware override if set, otherwise auto-detected
|
||||
const hardwareSlug = settings.hardwareSlug || hwInfo.slug
|
||||
|
||||
updateSession({
|
||||
fpsAvg: logResult.fpsAvg ?? null,
|
||||
fpsLow: logResult.fpsLow ?? null,
|
||||
fpsHigh: logResult.fpsHigh ?? null,
|
||||
fpsOnePercentLow: logResult.fpsOnePercentLow ?? null,
|
||||
tdpWatts: logResult.tdpWatts ?? null,
|
||||
hardwareSlug,
|
||||
hardwareName: hwInfo.name,
|
||||
osVersion,
|
||||
protonVersion,
|
||||
launchOptions,
|
||||
})
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
return (
|
||||
<PanelSection title="DeckyVault">
|
||||
<PanelSectionRow>
|
||||
<div className={staticClasses.Text} style={{ padding: "16px", textAlign: "center" }}>
|
||||
Loading...
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainPanel
|
||||
recordingState={recordingState}
|
||||
session={session}
|
||||
recentSessions={recentSessions}
|
||||
error={error}
|
||||
settings={settings}
|
||||
onStart={handleStart}
|
||||
onStop={handleStop}
|
||||
onUpdateSession={updateSession}
|
||||
onAddToRecent={addToRecent}
|
||||
onReset={reset}
|
||||
setError={setError}
|
||||
/>
|
||||
<SettingsPanel
|
||||
settings={settings}
|
||||
onUpdateSetting={updateSetting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default definePlugin(() => {
|
||||
return {
|
||||
name: "DeckyVault",
|
||||
content: () => {
|
||||
// Main plugin UI — will be implemented in a future phase
|
||||
return <div>DeckyVault Plugin</div>
|
||||
},
|
||||
onSettingUpdate: (newSettings: Partial<PluginSettings>) => {
|
||||
settings = { ...settings, ...newSettings }
|
||||
},
|
||||
onGameSessionStart: (appId: number) => {
|
||||
DeckyPlugin.log(`[DeckyVault] Game started: ${appId}`)
|
||||
// Future: start MangoHud monitoring
|
||||
},
|
||||
onGameSessionEnd: (appId: number) => {
|
||||
DeckyPlugin.log(`[DeckyVault] Game stopped: ${appId}`)
|
||||
// Future: stop monitoring, prompt export/upload
|
||||
titleView: <div className={staticClasses.Title}>DeckyVault</div>,
|
||||
content: <Content />,
|
||||
icon: <FaDatabase />,
|
||||
alwaysRender: false,
|
||||
onDismount() {
|
||||
console.log("[DeckyVault] Plugin unloading")
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { callable } from "@decky/api"
|
||||
|
||||
// ── Settings ────────────────────────────────────────────────────
|
||||
export const getSettings = callable<[], Record<string, unknown>>("get_settings")
|
||||
export const setSetting = callable<[key: string, value: unknown], Record<string, unknown>>("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<string, unknown>, exportPath: string], {
|
||||
success: boolean
|
||||
path: string
|
||||
error?: string
|
||||
}>("export_to_file")
|
||||
|
||||
// ── Upload ──────────────────────────────────────────────────────
|
||||
export const uploadToDeckyvault = callable<[
|
||||
data: Record<string, unknown>,
|
||||
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")
|
||||
@@ -0,0 +1,225 @@
|
||||
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<PluginSettings>(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<RecordingState>("idle")
|
||||
const [session, setSession] = useState<SessionData>(createEmptySession())
|
||||
const [recentSessions, setRecentSessions] = useState<RecentSession[]>([])
|
||||
const [error, setError] = useState<string>("")
|
||||
const currentAppIdRef = useRef<number | null>(null)
|
||||
const currentAppNameRef = useRef<string>("")
|
||||
|
||||
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<SessionData>) => {
|
||||
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 }
|
||||
Vendored
+10
-14
@@ -1,31 +1,27 @@
|
||||
// Decky Loader API type declarations
|
||||
// These mirror the APIs available in the Steam Deck game mode CEF context
|
||||
// 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 DeckyPlugin: {
|
||||
log: (...args: unknown[]) => void
|
||||
debug: (...args: unknown[]) => void
|
||||
info: (...args: unknown[]) => void
|
||||
error: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
const SteamClient: {
|
||||
Apps: {
|
||||
GetAppData: (appId: number) => Promise<{
|
||||
strAppName: string
|
||||
strShortcutName: string
|
||||
strExePath: string
|
||||
}>
|
||||
RegisterForGameStarted: (
|
||||
callback: (appId: number) => void,
|
||||
) => { unregister: () => void }
|
||||
RegisterForGameStopped: (
|
||||
callback: (appId: number) => void,
|
||||
) => { unregister: () => void }
|
||||
GetCurrentGameInfo: () => Promise<{
|
||||
appId: number
|
||||
strAppName: string
|
||||
}>
|
||||
}
|
||||
System: {
|
||||
GetOSVersion: () => Promise<string>
|
||||
}
|
||||
UI: {
|
||||
GetUIMode: () => Promise<number>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
@@ -0,0 +1,199 @@
|
||||
"""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: average the slowest 1% of frame times (largest frametimes),
|
||||
# then convert to FPS. Falls back to the lowest FPS percentile if no
|
||||
# frametime data is available.
|
||||
if frametime_values:
|
||||
sorted_ft = sorted(frametime_values)
|
||||
one_percent_count = max(1, int(len(sorted_ft) * 0.01))
|
||||
worst_ft = sorted_ft[-one_percent_count:]
|
||||
avg_worst_ft = sum(worst_ft) / len(worst_ft)
|
||||
fps_one_percent_low = round(1000.0 / avg_worst_ft, 1) if avg_worst_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.7 # avg of 15,15,14 = 14.667 -> 14.7
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
"""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)
|
||||
@@ -10,7 +10,10 @@
|
||||
"rootDir": "./src",
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["react", "react-dom"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
|
||||
Reference in New Issue
Block a user