diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c95962f..9fb2074 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,12 +66,6 @@ jobs: with: tagName: v__VERSION__ releaseName: "RelWave v__VERSION__" - # Let GitHub auto-generate the changelog from merged PRs and commits - # since the last release tag. The generated markdown is injected into - # latest.json as the `notes` field, which the in-app updater reads and - # displays in the What's New dialog after update. - releaseBody: "" - generateReleaseNotes: true releaseDraft: true prerelease: false args: ${{ matrix.args }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93f291d..579ca0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,27 @@ pnpm --dir bridge install pnpm tauri dev ``` +To build the bridge package for your platform, run one of these from the repo root: + +```bash +cd bridge +pnpm build:pkg:win +``` + +or on Linux: + +```bash +cd bridge +pnpm build:pkg:linux +``` + If you are working on bridge-related code, you can run bridge tests directly: ```bash cd bridge + +docker compose -f docker-compose.test.yml up -d + pnpm test ``` diff --git a/FEATURES.md b/FEATURES.md index af6e460..2df8aaf 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -50,9 +50,12 @@ The main landing page for managing database connections. Features a clean, IDE-i **Connection Management** - Add new database connections with detailed configuration (name, type, host, port, user, password, SSL options) +- **SSH Tunneling** — connect securely to remote databases via SSH tunnels using private keys and passphrases - **SQLite support** — connect to local `.db`, `.sqlite`, `.sqlite3`, `.s3db` files via native file picker - Connect via URL — paste connection strings like `postgres://user:pass@host:port/db` - Auto-parse URLs to populate connection form fields (including `sqlite://` protocol) +- **Safe connection deletion** — intercepts deletion to prevent accidental loss of associated project data (schemas, queries, diagrams) +- **Unlinked project management** — view, delete, or relink orphaned projects to new connections directly from the sidebar - Delete existing database connections - Test connections with real-time feedback - Connection status indicators for all databases @@ -376,6 +379,30 @@ RelWave includes native Git integration powered by `simple-git`, providing a ful --- +## AI Features and Providers + +RelWave integrates deeply with multiple Large Language Models (LLMs) to provide intelligent database assistance, schema analysis, and query explanations. + +### Supported Providers +The application supports a flexible, multi-provider AI architecture: +- **OpenAI** (GPT-4o, GPT-4 Turbo, etc.) +- **Anthropic** (Claude 3.5 Sonnet, Opus, etc.) +- **Mistral** (Mistral Large, Mistral Small, etc.) +- **Groq** (Llama 3, Mixtral, etc. for ultra-fast inference) +- **Ollama** (Local models like Llama 3, Phi-3, Mistral) +- **Google Gemini** (Gemini 1.5 Pro, Flash) + +### AI Capabilities +| Feature | Description | +| ------- | ----------- | +| **Schema Analysis** | Analyzes the structure of your database, identifying relationships, potential optimizations, and providing a human-readable summary of complex schemas. Uses a highly optimized, token-efficient dense schema representation. | +| **Query Explanation** | Breaks down complex SQL queries into plain English, explaining joins, filters, performance implications, and the overall intent of the query. | +| **Local AI Support** | Full privacy and zero-cost inference available through local Ollama integration, ensuring sensitive database schemas never leave your machine. | +| **Context Aware** | The AI system automatically receives the dialect (PostgreSQL, MySQL, SQLite) and the relevant database schema context to provide accurate, dialect-specific responses. | +| **High Token Capacity** | Configured with large output windows (up to 4096 tokens) to handle extensive schema analyses and complex query breakdowns without truncation. | + +--- + ## Visual Tools ### Chart Visualization @@ -642,6 +669,6 @@ All database and Git operations use a JSON-RPC protocol over stdin/stdout. The b --- -**Last Updated:** May 2026 +**Last Updated:** June 2026 This document is maintained alongside the application and updated with each release. diff --git a/README.md b/README.md index 02ce0e6..141b213 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ _A high-performance, cross-platform desktop suite for developers who demand more from their database tools._ -[![Version](https://img.shields.io/badge/version-0.7.0--beta.1-0066ff?style=for-the-badge&logo=semver)](https://github.com/Relwave/relwave-app/releases) +[![Version](https://img.shields.io/badge/version-0.9.0--rc.1-0066ff?style=for-the-badge&logo=semver)](https://github.com/Relwave/relwave-app/releases) [![License](https://img.shields.io/badge/license-MIT-00cc66?style=for-the-badge)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-lightgray?style=for-the-badge&logo=windows)](https://github.com/Relwave/relwave-app/releases) [![Tauri](https://img.shields.io/badge/built%20with-Tauri-FFC131?style=for-the-badge&logo=tauri)](https://tauri.app/) diff --git a/bridge/__tests__/projectStore.test.ts b/bridge/__tests__/projectStore.test.ts index a3a5f2d..8acecda 100644 --- a/bridge/__tests__/projectStore.test.ts +++ b/bridge/__tests__/projectStore.test.ts @@ -257,9 +257,12 @@ describe("ProjectStore", () => { name: "users", type: "BASE TABLE", columns: [ - { name: "id", type: "integer", nullable: false, isPrimaryKey: true, isForeignKey: false, defaultValue: null, isUnique: true }, - { name: "email", type: "varchar(255)", nullable: false, isPrimaryKey: false, isForeignKey: false, defaultValue: null, isUnique: true }, + { name: "id", type: "integer", nullable: false, isPrimaryKey: true, isForeignKey: false, defaultValue: null, isUnique: true, isSerial: true, ordinalPosition: 1 }, + { name: "email", type: "varchar(255)", nullable: false, isPrimaryKey: false, isForeignKey: false, defaultValue: null, isUnique: true, isSerial: false, ordinalPosition: 2 }, ], + indexes: [], + foreignKeys: [], + checks: [], }, ], }, @@ -301,8 +304,11 @@ describe("ProjectStore", () => { name: "posts", type: "BASE TABLE", columns: [ - { name: "id", type: "integer", nullable: false, isPrimaryKey: true, isForeignKey: false, defaultValue: null, isUnique: true }, + { name: "id", type: "integer", nullable: false, isPrimaryKey: true, isForeignKey: false, defaultValue: null, isUnique: true, isSerial: true, ordinalPosition: 1 }, ], + indexes: [], + foreignKeys: [], + checks: [], }, ], }, diff --git a/bridge/package.json b/bridge/package.json index 1fb23b9..d4f078a 100644 --- a/bridge/package.json +++ b/bridge/package.json @@ -1,32 +1,39 @@ { "name": "relwave-bridge", - "version": "0.7.0-beta-1", + "version": " 0.9.0-rc-1", "type": "commonjs", "main": "dist/index.cjs", "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", "start": "node dist/index.cjs", "build": "tsc --project tsconfig.json && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --format=cjs --packages=external", + "build:bundle-for-pkg": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.bundled.cjs --format=cjs --external:better-sqlite3 --external:@napi-rs/keyring --external:ssh2 --external:cpu-features --external:pg-native", "copy:native": "node scripts/copy-native.js", "rebuild:native": "npm rebuild better-sqlite3", - "build:pkg:win": "npm run build && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg . --target node22-win-x64 --output ../src-tauri/resources/bridge-x86_64-pc-windows-msvc.exe", - "build:pkg:linux": "npm run build && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg . --target node22-linux-x64 --output ../src-tauri/resources/bridge-x86_64-unknown-linux-gnu", + "build:pkg:win": "npm run build && npm run build:bundle-for-pkg && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg dist/index.bundled.cjs --target node22-win-x64 --output ../src-tauri/resources/bridge-x86_64-pc-windows-msvc.exe", + "build:pkg:linux": "npm run build && npm run build:bundle-for-pkg && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg dist/index.bundled.cjs --target node22-linux-x64 --output ../src-tauri/resources/bridge-x86_64-unknown-linux-gnu", "test": "jest", "test:watch": "jest --watchAll --detectOpenHandles" }, "dependencies": { + "@anthropic-ai/sdk": "^0.100.1", + "@google/generative-ai": "^0.24.1", "@jest/globals": "^30.2.0", + "@mistralai/mistralai": "^2.2.5", "@napi-rs/keyring": "^1.2.0", "@types/ssh2": "^1.15.5", "bcryptjs": "^3.0.3", "better-sqlite3": "^11.9.0", "dotenv": "^17.2.3", + "groq-sdk": "^1.2.1", "mysql2": "^3.15.3", + "ollama": "^0.6.3", + "openai": "^6.41.0", "pg": "^8.16.3", "pg-query-stream": "^4.10.3", "pino": "^9.14.0", "ssh2": "^1.17.0", - "uuid": "^8.3.2", + "uuid": "^9.0.1", "ws": "^8.19.0" }, "bin": "./dist/index.cjs", @@ -53,4 +60,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.0.0" } -} +} \ No newline at end of file diff --git a/bridge/pnpm-lock.yaml b/bridge/pnpm-lock.yaml index 9f5d41d..2b63130 100644 --- a/bridge/pnpm-lock.yaml +++ b/bridge/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: dependencies: + '@anthropic-ai/sdk': + specifier: ^0.100.1 + version: 0.100.1(zod@4.4.3) + '@google/generative-ai': + specifier: ^0.24.1 + version: 0.24.1 '@jest/globals': specifier: ^30.2.0 version: 30.2.0 + '@mistralai/mistralai': + specifier: ^2.2.5 + version: 2.2.5 '@napi-rs/keyring': specifier: ^1.2.0 version: 1.2.0 @@ -22,19 +31,28 @@ importers: version: 3.0.3 better-sqlite3: specifier: ^11.9.0 - version: 11.9.0 + version: 11.10.0 dotenv: specifier: ^17.2.3 - version: 17.2.3 + version: 17.4.2 + groq-sdk: + specifier: ^1.2.1 + version: 1.2.1 mysql2: specifier: ^3.15.3 version: 3.15.3 + ollama: + specifier: ^0.6.3 + version: 0.6.3 + openai: + specifier: ^6.41.0 + version: 6.41.0(ws@8.20.1)(zod@4.4.3) pg: specifier: ^8.16.3 - version: 8.16.3 + version: 8.21.0 pg-query-stream: specifier: ^4.10.3 - version: 4.10.3(pg@8.16.3) + version: 4.10.3(pg@8.21.0) pino: specifier: ^9.14.0 version: 9.14.0 @@ -42,11 +60,11 @@ importers: specifier: ^1.17.0 version: 1.17.0 uuid: - specifier: ^8.3.2 - version: 8.3.2 + specifier: ^9.0.1 + version: 9.0.1 ws: specifier: ^8.19.0 - version: 8.19.0 + version: 8.20.1 devDependencies: '@types/better-sqlite3': specifier: ^7.6.13 @@ -56,70 +74,79 @@ importers: version: 30.0.0 '@types/node': specifier: ^20.0.0 - version: 20.19.25 + version: 20.19.41 '@types/pg': specifier: ^8.15.6 - version: 8.15.6 + version: 8.20.0 '@yao-pkg/pkg': specifier: ^5.12.0 version: 5.16.1 esbuild: specifier: ^0.27.0 - version: 0.27.0 + version: 0.27.7 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 - version: 2.0.0(@types/node@20.19.25)(typescript@5.9.3) + version: 2.0.0(@types/node@20.19.41)(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 packages: - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@anthropic-ai/sdk@0.100.1': + resolution: {integrity: sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.5': - resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.27.1': @@ -134,12 +161,12 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true @@ -180,8 +207,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -228,22 +255,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -253,171 +284,175 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@google/generative-ai@0.24.1': + resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==} + engines: {node: '>=18.0.0'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -531,6 +566,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mistralai/mistralai@2.2.5': + resolution: {integrity: sha512-ATbWzKkNzNAZ+gtw9MI/c/ULTMG80tKUiRNIbQFfg4OP0uEZZpTfXZeBCNfs5Dq0uqMQ/tQWc4o6RRJQtMrpDA==} + '@napi-rs/keyring-darwin-arm64@1.2.0': resolution: {integrity: sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==} engines: {node: '>= 10'} @@ -630,6 +668,9 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -675,11 +716,11 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.25': - resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/node@20.19.41': + resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} - '@types/pg@8.15.6': - resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} '@types/ssh2@1.15.5': resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} @@ -809,8 +850,8 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true @@ -894,8 +935,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.5: - resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} hasBin: true bcrypt-pbkdf@1.0.2: @@ -905,8 +947,8 @@ packages: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true - better-sqlite3@11.9.0: - resolution: {integrity: sha512-4b9xYnoaskj8eIkke9ZCB42p5bOPabptSku8Rl4Yww70Jf+aHeLvrIjXDJrKQxUEjdppsFb+fdJSjoH4TklROA==} + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -918,18 +960,18 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -962,8 +1004,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -980,8 +1022,8 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - ci-info@4.3.1: - resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} cjs-module-lexer@2.1.1: @@ -1041,8 +1083,8 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -1069,12 +1111,12 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} dynamic-dedupe@0.3.0: @@ -1083,8 +1125,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -1102,8 +1144,12 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -1139,6 +1185,9 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -1220,6 +1269,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + groq-sdk@1.2.1: + resolution: {integrity: sha512-dsDSWJRJf+n2dPiCv7zU3IsJbrh7jfSPqi6vc1q0TTK1oUF6bn+wv4P2VFdynkHpuJ0TTJ57vlpT87judPgVPA==} + hasBin: true + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1244,8 +1297,8 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -1484,6 +1537,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1512,12 +1569,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - lru.min@1.1.3: - resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} make-dir@4.0.0: @@ -1545,11 +1598,11 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -1577,9 +1630,9 @@ packages: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} engines: {node: '>= 8.0'} - named-placeholders@1.1.3: - resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} - engines: {node: '>=12.0.0'} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} nan@2.25.0: resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} @@ -1598,8 +1651,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-abi@3.85.0: - resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} node-fetch@2.7.0: @@ -1614,8 +1667,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -1625,6 +1679,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + ollama@0.6.3: + resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1636,6 +1693,17 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@6.41.0: + resolution: {integrity: sha512-IGWPopZq6Rjoynjfb3NSLf/z2MTw7UiOsm9TAjPGAjUESH7Uq41Trg4QWehBEn58p74i+m7uoRPV2vXcpPXhyA==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + p-is-promise@3.0.0: resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} engines: {node: '>=8'} @@ -1682,11 +1750,11 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - pg-cloudflare@1.2.7: - resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} pg-cursor@2.15.3: resolution: {integrity: sha512-eHw63TsiGtFEfAd7tOTZ+TLy+i/2ePKS20H84qCQ+aQ60pve05Okon9tKMC+YN3j6XyeFoHnaim7Lt9WVafQsA==} @@ -1697,13 +1765,13 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.10.1: - resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} peerDependencies: pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.14.0: + resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} pg-query-stream@4.10.3: resolution: {integrity: sha512-h2utrzpOIzeT9JfaqfvBbVuvCfBjH86jNfVrGGTbyepKAIOyTfDew0lAt8bbJjs9n/I5bGDl7S2sx6h5hPyJxw==} @@ -1714,8 +1782,8 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.16.3: - resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + pg@8.21.0: + resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -1729,12 +1797,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pino-abstract-transport@2.0.0: @@ -1759,8 +1827,8 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} engines: {node: '>=0.10.0'} postgres-date@1.0.7: @@ -1791,8 +1859,8 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -1834,8 +1902,8 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true @@ -1861,8 +1929,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -1926,6 +1994,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + stream-meter@1.0.4: resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==} @@ -1951,8 +2022,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -2005,8 +2076,8 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tmpl@1.0.5: @@ -2023,6 +2094,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-jest@29.4.6: resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -2118,8 +2192,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.2.2: - resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2127,8 +2201,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -2144,6 +2219,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -2170,8 +2248,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2217,27 +2295,42 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: - '@babel/code-frame@7.27.1': + '@anthropic-ai/sdk@0.100.1(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 4.4.3 + + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.5': {} + '@babel/compat-data@7.29.0': {} - '@babel/core@7.28.5': + '@babel/core@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -2247,41 +2340,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.5': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.28.5 + '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-imports@7.27.1': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} @@ -2289,119 +2382,121 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.4': + '@babel/helpers@7.28.6': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 - '@babel/parser@7.28.5': + '@babel/parser@7.29.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/template@7.27.2': + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 - '@babel/traverse@7.28.5': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.5': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 @@ -2412,105 +2507,107 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.7.1': + '@emnapi/core@1.10.0': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.27.7': optional: true + '@google/generative-ai@0.24.1': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -2528,13 +2625,13 @@ snapshots: '@jest/console@30.2.0': dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 jest-message-util: 30.2.0 jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@30.2.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + '@jest/core@30.2.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -2542,14 +2639,14 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 4.3.1 + ci-info: 4.4.0 exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -2576,7 +2673,7 @@ snapshots: dependencies: '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 jest-mock: 30.2.0 '@jest/expect-utils@30.2.0': @@ -2594,7 +2691,7 @@ snapshots: dependencies: '@jest/types': 30.2.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 20.19.25 + '@types/node': 20.19.41 jest-message-util: 30.2.0 jest-mock: 30.2.0 jest-util: 30.2.0 @@ -2612,7 +2709,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 20.19.25 + '@types/node': 20.19.41 jest-regex-util: 30.0.1 '@jest/reporters@30.2.0': @@ -2623,7 +2720,7 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -2676,7 +2773,7 @@ snapshots: '@jest/transform@30.2.0': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/types': 30.2.0 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 7.0.1 @@ -2700,7 +2797,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.25 + '@types/node': 20.19.41 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -2728,6 +2825,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mistralai/mistralai@2.2.5': + dependencies: + ws: 8.20.1 + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@napi-rs/keyring-darwin-arm64@1.2.0': optional: true @@ -2781,8 +2887,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -2803,6 +2909,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@stablelib/base64@1.0.1': {} + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -2818,28 +2926,28 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 20.19.25 + '@types/node': 20.19.41 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2860,14 +2968,14 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.25': + '@types/node@20.19.41': dependencies: undici-types: 6.21.0 - '@types/pg@8.15.6': + '@types/pg@8.20.0': dependencies: - '@types/node': 20.19.25 - pg-protocol: 1.10.3 + '@types/node': 20.19.41 + pg-protocol: 1.14.0 pg-types: 2.2.0 '@types/ssh2@1.15.5': @@ -2953,7 +3061,7 @@ snapshots: node-fetch: 2.7.0 picocolors: 1.1.1 progress: 2.0.3 - semver: 7.7.3 + semver: 7.8.1 tar-fs: 2.1.4 yargs: 16.2.0 transitivePeerDependencies: @@ -2962,28 +3070,28 @@ snapshots: '@yao-pkg/pkg@5.16.1': dependencies: - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@yao-pkg/pkg-fetch': 3.5.16 into-stream: 6.0.0 minimist: 1.2.8 multistream: 4.1.0 picocolors: 1.1.1 - picomatch: 4.0.3 + picomatch: 4.0.4 prebuild-install: 7.1.3 - resolve: 1.22.11 + resolve: 1.22.12 stream-meter: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 transitivePeerDependencies: - encoding - supports-color acorn-walk@8.3.4: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn@8.15.0: {} + acorn@8.16.0: {} agent-base@6.0.2: dependencies: @@ -3010,7 +3118,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arg@4.1.3: {} @@ -3026,13 +3134,13 @@ snapshots: aws-ssl-profiles@1.1.2: {} - babel-jest@30.2.0(@babel/core@7.28.5): + babel-jest@30.2.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/transform': 30.2.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.2.0(@babel/core@7.28.5) + babel-preset-jest: 30.2.0(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -3041,7 +3149,7 @@ snapshots: babel-plugin-istanbul@7.0.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 6.0.3 @@ -3053,36 +3161,36 @@ snapshots: dependencies: '@types/babel__core': 7.20.5 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) - - babel-preset-jest@30.2.0(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@30.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 30.2.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) balanced-match@1.0.2: {} base64-js@1.5.1: {} - baseline-browser-mapping@2.9.5: {} + baseline-browser-mapping@2.10.31: {} bcrypt-pbkdf@1.0.2: dependencies: @@ -3090,7 +3198,7 @@ snapshots: bcryptjs@3.0.3: {} - better-sqlite3@11.9.0: + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -3107,12 +3215,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - brace-expansion@1.1.12: + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 @@ -3120,13 +3228,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.9.5 - caniuse-lite: 1.0.30001760 - electron-to-chromium: 1.5.267 - node-releases: 2.0.27 - update-browserslist-db: 1.2.2(browserslist@4.28.1) + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) bs-logger@0.2.6: dependencies: @@ -3152,7 +3260,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001793: {} chalk@4.1.2: dependencies: @@ -3175,7 +3283,7 @@ snapshots: chownr@1.1.4: {} - ci-info@4.3.1: {} + ci-info@4.4.0: {} cjs-module-lexer@2.1.1: {} @@ -3229,7 +3337,7 @@ snapshots: dependencies: mimic-response: 3.1.0 - dedent@1.7.0: {} + dedent@1.7.2: {} deep-extend@0.6.0: {} @@ -3241,9 +3349,9 @@ snapshots: detect-newline@3.1.0: {} - diff@4.0.2: {} + diff@4.0.4: {} - dotenv@17.2.3: {} + dotenv@17.4.2: {} dynamic-dedupe@0.3.0: dependencies: @@ -3251,7 +3359,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.361: {} emittery@0.13.1: {} @@ -3267,34 +3375,36 @@ snapshots: dependencies: is-arrayish: 0.2.1 - esbuild@0.27.0: + es-errors@1.3.0: {} + + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 + '@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 escalade@3.2.0: {} @@ -3329,13 +3439,15 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-sha256@1.3.0: {} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-uri-to-path@1.0.0: {} @@ -3389,7 +3501,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.9 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -3399,12 +3511,14 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 graceful-fs@4.2.11: {} + groq-sdk@1.2.1: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -3431,7 +3545,7 @@ snapshots: human-signals@2.1.0: {} - iconv-lite@0.7.0: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -3492,11 +3606,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -3537,10 +3651,10 @@ snapshots: '@jest/expect': 30.2.0 '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 co: 4.6.0 - dedent: 1.7.0 + dedent: 1.7.2 is-generator-fn: 2.1.0 jest-each: 30.2.0 jest-matcher-utils: 30.2.0 @@ -3557,15 +3671,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -3576,16 +3690,16 @@ snapshots: - supports-color - ts-node - jest-config@30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 '@jest/pattern': 30.0.1 '@jest/test-sequencer': 30.2.0 '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) + babel-jest: 30.2.0(@babel/core@7.29.0) chalk: 4.1.2 - ci-info: 4.3.1 + ci-info: 4.4.0 deepmerge: 4.3.1 glob: 10.5.0 graceful-fs: 4.2.11 @@ -3603,8 +3717,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.19.25 - ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) + '@types/node': 20.19.41 + ts-node: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -3633,7 +3747,7 @@ snapshots: '@jest/environment': 30.2.0 '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 jest-mock: 30.2.0 jest-util: 30.2.0 jest-validate: 30.2.0 @@ -3641,7 +3755,7 @@ snapshots: jest-haste-map@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -3667,7 +3781,7 @@ snapshots: jest-message-util@30.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@jest/types': 30.2.0 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -3680,7 +3794,7 @@ snapshots: jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 jest-util: 30.2.0 jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): @@ -3714,7 +3828,7 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -3743,7 +3857,7 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 cjs-module-lexer: 2.1.1 collect-v8-coverage: 1.0.3 @@ -3763,17 +3877,17 @@ snapshots: jest-snapshot@30.2.0: dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) - '@babel/types': 7.28.5 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 '@jest/expect-utils': 30.2.0 '@jest/get-type': 30.1.0 '@jest/snapshot-utils': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) chalk: 4.1.2 expect: 30.2.0 graceful-fs: 4.2.11 @@ -3782,7 +3896,7 @@ snapshots: jest-message-util: 30.2.0 jest-util: 30.2.0 pretty-format: 30.2.0 - semver: 7.7.3 + semver: 7.8.1 synckit: 0.11.11 transitivePeerDependencies: - supports-color @@ -3790,11 +3904,11 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 chalk: 4.1.2 - ci-info: 4.3.1 + ci-info: 4.4.0 graceful-fs: 4.2.11 - picomatch: 4.0.3 + picomatch: 4.0.4 jest-validate@30.2.0: dependencies: @@ -3809,7 +3923,7 @@ snapshots: dependencies: '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 20.19.25 + '@types/node': 20.19.41 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3818,18 +3932,18 @@ snapshots: jest-worker@30.2.0: dependencies: - '@types/node': 20.19.25 + '@types/node': 20.19.41 '@ungap/structured-clone': 1.3.0 jest-util: 30.2.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest@30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3848,6 +3962,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json5@2.2.3: {} leven@3.1.0: {} @@ -3868,13 +3987,11 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - - lru.min@1.1.3: {} + lru.min@1.1.4: {} make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.8.1 make-error@1.3.6: {} @@ -3887,19 +4004,19 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mimic-fn@2.1.0: {} mimic-response@3.1.0: {} - minimatch@3.1.2: + minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.14 - minimatch@9.0.5: + minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.0 minimist@1.2.8: {} @@ -3921,16 +4038,16 @@ snapshots: aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 long: 5.3.2 - lru.min: 1.1.3 - named-placeholders: 1.1.3 + lru.min: 1.1.4 + named-placeholders: 1.1.6 seq-queue: 0.0.5 sqlstring: 2.3.3 - named-placeholders@1.1.3: + named-placeholders@1.1.6: dependencies: - lru-cache: 7.18.3 + lru.min: 1.1.4 nan@2.25.0: optional: true @@ -3943,9 +4060,9 @@ snapshots: neo-async@2.6.2: {} - node-abi@3.85.0: + node-abi@3.87.0: dependencies: - semver: 7.7.3 + semver: 7.8.1 node-fetch@2.7.0: dependencies: @@ -3953,7 +4070,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.27: {} + node-releases@2.0.46: {} normalize-path@3.0.0: {} @@ -3961,6 +4078,10 @@ snapshots: dependencies: path-key: 3.1.1 + ollama@0.6.3: + dependencies: + whatwg-fetch: 3.6.20 + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -3971,6 +4092,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@6.41.0(ws@8.20.1)(zod@4.4.3): + optionalDependencies: + ws: 8.20.1 + zod: 4.4.3 + p-is-promise@3.0.0: {} p-limit@2.3.0: @@ -3991,7 +4117,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -4009,45 +4135,45 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - pg-cloudflare@1.2.7: + pg-cloudflare@1.4.0: optional: true - pg-connection-string@2.9.1: {} + pg-connection-string@2.13.0: {} - pg-cursor@2.15.3(pg@8.16.3): + pg-cursor@2.15.3(pg@8.21.0): dependencies: - pg: 8.16.3 + pg: 8.21.0 pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-pool@3.14.0(pg@8.21.0): dependencies: - pg: 8.16.3 + pg: 8.21.0 - pg-protocol@1.10.3: {} + pg-protocol@1.14.0: {} - pg-query-stream@4.10.3(pg@8.16.3): + pg-query-stream@4.10.3(pg@8.21.0): dependencies: - pg: 8.16.3 - pg-cursor: 2.15.3(pg@8.16.3) + pg: 8.21.0 + pg-cursor: 2.15.3(pg@8.21.0) pg-types@2.2.0: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 - postgres-bytea: 1.0.0 + postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.16.3: + pg@8.21.0: dependencies: - pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.16.3) - pg-protocol: 1.10.3 + pg-connection-string: 2.13.0 + pg-pool: 3.14.0(pg@8.21.0) + pg-protocol: 1.14.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.2.7 + pg-cloudflare: 1.4.0 pgpass@1.0.5: dependencies: @@ -4055,9 +4181,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pino-abstract-transport@2.0.0: dependencies: @@ -4087,7 +4213,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -4103,8 +4229,8 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.85.0 - pump: 3.0.3 + node-abi: 3.87.0 + pump: 3.0.4 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.4 @@ -4122,7 +4248,7 @@ snapshots: progress@2.0.3: {} - pump@3.0.3: + pump@3.0.4: dependencies: end-of-stream: 1.4.5 once: 1.4.0 @@ -4158,7 +4284,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 real-require@0.2.0: {} @@ -4170,8 +4296,9 @@ snapshots: resolve-from@5.0.0: {} - resolve@1.22.11: + resolve@1.22.12: dependencies: + es-errors: 1.3.0 is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -4190,7 +4317,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} + semver@7.8.1: {} seq-queue@0.0.5: {} @@ -4248,6 +4375,11 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + stream-meter@1.0.4: dependencies: readable-stream: 2.3.8 @@ -4267,7 +4399,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string_decoder@1.1.1: dependencies: @@ -4281,7 +4413,7 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.2: + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -4313,7 +4445,7 @@ snapshots: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.3 + pump: 3.0.4 tar-stream: 2.2.0 tar-stream@2.2.0: @@ -4328,16 +4460,16 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 thread-stream@3.1.0: dependencies: real-require: 0.2.0 - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tmpl@1.0.5: {} @@ -4349,38 +4481,40 @@ snapshots: tree-kill@1.2.2: {} - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(typescript@5.9.3): + ts-algebra@2.0.0: {} + + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest: 30.2.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.3 + semver: 7.8.1 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - esbuild: 0.27.0 + babel-jest: 30.2.0(@babel/core@7.29.0) + esbuild: 0.27.7 jest-util: 30.2.0 - ts-node-dev@2.0.0(@types/node@20.19.25)(typescript@5.9.3): + ts-node-dev@2.0.0(@types/node@20.19.41)(typescript@5.9.3): dependencies: chokidar: 3.6.0 dynamic-dedupe: 0.3.0 minimist: 1.2.8 mkdirp: 1.0.4 - resolve: 1.22.11 + resolve: 1.22.12 rimraf: 2.7.1 source-map-support: 0.5.21 tree-kill: 1.2.2 - ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) + ts-node: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) tsconfig: 7.0.0 typescript: 5.9.3 transitivePeerDependencies: @@ -4388,19 +4522,19 @@ snapshots: - '@swc/wasm' - '@types/node' - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): + ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.19.25 - acorn: 8.15.0 + '@types/node': 20.19.41 + acorn: 8.16.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 - diff: 4.0.2 + diff: 4.0.4 make-error: 1.3.6 typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 @@ -4461,15 +4595,15 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.2.2(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 util-deprecate@1.0.2: {} - uuid@8.3.2: {} + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -4485,6 +4619,8 @@ snapshots: webidl-conversions@3.0.1: {} + whatwg-fetch@3.6.20: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -4506,7 +4642,7 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrappy@1.0.2: {} @@ -4515,7 +4651,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.19.0: {} + ws@8.20.1: {} xtend@4.0.2: {} @@ -4550,3 +4686,9 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.1(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/bridge/src/ai/README.md b/bridge/src/ai/README.md new file mode 100644 index 0000000..0746cdf --- /dev/null +++ b/bridge/src/ai/README.md @@ -0,0 +1,23 @@ +# AI Integration (Bridge) + +This folder contains AI provider implementations and prompt templates used by the bridge. + +Layout +- `providers/` — concrete provider adapters (OpenAI, Anthropic, Gemini, Mistral, Ollama, Groq). Implement the `AIProvider` interface in `providers/types.ts`. +- `prompts/` — prompt builders and parsers for schema analysis, query explanation and chart recommendation. + +What moved +- The public RPC entry points and handler logic live under `src/handlers/aiHandlers.ts`. +- The service factory implementation was moved to `src/services/ai.impl.ts` and a shim `src/services/aiService.ts` exposes `AIService` and `aiService` for consistency with other bridge services. +- Shared AI types were moved to `src/types/ai.ts` and are re-exported from the central `src/types/index.ts`. + +How to add a provider +1. Create a new file under `providers/` implementing the `AIProvider` interface. +2. Add the provider into `src/services/ai.impl.ts` in the `createProvider` switch. +3. Add any provider-specific configuration notes to this README. + +Testing +- No tests currently exist for AI. To exercise the integration locally, use the frontend UI features that call `ai.*` RPC methods or create a test that uses the RPC registrar. + +Notes +- Keep providers small and avoid direct network retries; the bridge surface should translate provider errors into `AIError` using `providers/types.ts`. diff --git a/bridge/src/ai/prompts/chart-recommendation.ts b/bridge/src/ai/prompts/chart-recommendation.ts new file mode 100644 index 0000000..bb5316e --- /dev/null +++ b/bridge/src/ai/prompts/chart-recommendation.ts @@ -0,0 +1,66 @@ +import { ChartRecommendationInput, ChartRecommendation } from "../../types/"; +import { SYSTEM_CONTEXT } from "./shared"; +import { AIError } from "../providers/types"; + +export function buildChartRecommendationPrompt(input: ChartRecommendationInput): { + system: string; + user: string; +} { + const columnList = input.columns + .map((c) => { + const flags: string[] = [c.type]; + if (c.isPrimaryKey) flags.push("PK"); + if (c.sampleValues?.length) flags.push(`samples: ${c.sampleValues.slice(0, 3).join(", ")}`); + return `- ${c.name} (${flags.join(", ")})`; + }) + .join("\n"); + + const user = `Given the table "${input.tableName}" with the following columns, recommend the best chart visualization: + +## Columns +${columnList} + +## Instructions +Respond ONLY with a valid JSON object — no markdown fences, no explanation text. +The JSON must match exactly this shape: +{ + "chartType": "bar" | "line" | "area" | "pie", + "xAxis": "", + "yAxis": "", + "reasoning": "" +} + +Choose the most insightful combination. Prefer grouping a categorical/text column on X and counting a numeric/PK column on Y.`; + + return { system: SYSTEM_CONTEXT, user }; +} + +/** + * Parse the raw LLM text response into a ChartRecommendation. + * The model is instructed to return bare JSON, but defensively strip + * any markdown fences if the model adds them anyway. + */ +export function parseChartRecommendation(raw: string): ChartRecommendation { + // Strip markdown code fences if present + const cleaned = raw + .replace(/```json\s*/gi, "") + .replace(/```\s*/g, "") + .trim(); + + let parsed: any; + try { + parsed = JSON.parse(cleaned); + } catch { + throw new AIError("PARSE_ERROR", "chart-recommendation", `Failed to parse chart recommendation JSON: ${raw.slice(0, 200)}`); + } + + const validTypes = ["bar", "line", "area", "pie"]; + const chartType = validTypes.includes(parsed.chartType) ? parsed.chartType : "bar"; + + return { + chartType: chartType as ChartRecommendation["chartType"], + xAxis: String(parsed.xAxis ?? ""), + yAxis: String(parsed.yAxis ?? ""), + reasoning: String(parsed.reasoning ?? ""), + }; +} diff --git a/bridge/src/ai/prompts/query-explanation.ts b/bridge/src/ai/prompts/query-explanation.ts new file mode 100644 index 0000000..1897456 --- /dev/null +++ b/bridge/src/ai/prompts/query-explanation.ts @@ -0,0 +1,42 @@ +import { QueryExplanationInput } from "../../types/"; +import { SYSTEM_CONTEXT, MARKDOWN_INSTRUCTION } from "./shared"; + +export function buildQueryExplanationPrompt(input: QueryExplanationInput): { + system: string; + user: string; +} { + const schemaContext = + input.schema && input.schema.length > 0 + ? input.schema + .map((t) => { + const cols = t.columns + .map((c) => `${c.name} ${c.type}${c.isPrimaryKey ? " PK" : ""}${c.isForeignKey ? " FK" : ""}`) + .join(", "); + return `- ${t.name}(${cols})`; + }) + .join("\n") + : "Schema not provided."; + + const dbType = input.databaseType ? ` (${input.databaseType})` : ""; + + const user = `Explain the following SQL query${dbType}: + +\`\`\`sql +${input.sql} +\`\`\` + +## Relevant Schema +${schemaContext} + +## What to cover +1. **Query Purpose** — What does this query do in plain English? +2. **Joins** — Explain any joins and the relationships they traverse +3. **Filters** — What data is being filtered and why +4. **Aggregations** — Any GROUP BY, COUNT, SUM, etc., and what they compute +5. **Performance Concerns** — Potential bottlenecks, missing indexes, full table scans +6. **Suggested Improvements** — Rewritten or optimized version if applicable + +${MARKDOWN_INSTRUCTION}`; + + return { system: SYSTEM_CONTEXT, user }; +} diff --git a/bridge/src/ai/prompts/schema-analysis.ts b/bridge/src/ai/prompts/schema-analysis.ts new file mode 100644 index 0000000..8d9a79d --- /dev/null +++ b/bridge/src/ai/prompts/schema-analysis.ts @@ -0,0 +1,52 @@ +import { SchemaAnalysisInput } from "../../types/"; +import { SYSTEM_CONTEXT, MARKDOWN_INSTRUCTION } from "./shared"; + +export function buildSchemaAnalysisPrompt(input: SchemaAnalysisInput): { + system: string; + user: string; +} { + const tableDescriptions = input.tables + .map((t) => { + const columns = t.columns + .map((c) => { + const flags: string[] = []; + if (c.isPrimaryKey) flags.push("PK"); + if (c.isForeignKey) flags.push("FK"); + if (!c.nullable) flags.push("NOT NULL"); + if (c.references) flags.push(`→${c.references.table}.${c.references.column}`); + return `${c.name}(${c.type})${flags.length ? "[" + flags.join(",") + "]" : ""}`; + }) + .join(", "); + + const extras: string[] = []; + if (t.indexes?.length) extras.push(`idx: ${t.indexes.join(",")}`); + if (t.foreignKeys?.length) extras.push(`fk: ${t.foreignKeys.join(",")}`); + if (t.constraints?.length) extras.push(`cons: ${t.constraints.join(",")}`); + + const tableName = `${t.schema ? `${t.schema}.` : ""}${t.name}`; + const extrasStr = extras.length ? ` | ${extras.join(" | ")}` : ""; + + return `[${tableName}] cols: ${columns}${extrasStr}`; + }) + .join("\n"); + + const dbType = input.databaseType ? ` (${input.databaseType})` : ""; + + const user = `Analyze the following database schema${dbType} and provide: + +## What to cover +1. **Purpose** — What does this database appear to be for? +2. **Architecture** — Key design decisions and table relationships +3. **Missing Indexes** — Columns that should be indexed but aren't +4. **Schema Smells** — Anti-patterns, naming issues, or poor design choices +5. **Normalization Concerns** — Over/under-normalization issues +6. **Scalability Concerns** — Issues that may cause problems at scale +7. **Suggested Improvements** — Concrete, actionable recommendations + +## Schema +${tableDescriptions || "No tables provided."} + +${MARKDOWN_INSTRUCTION}`; + + return { system: SYSTEM_CONTEXT, user }; +} diff --git a/bridge/src/ai/prompts/shared.ts b/bridge/src/ai/prompts/shared.ts new file mode 100644 index 0000000..2a7bcc6 --- /dev/null +++ b/bridge/src/ai/prompts/shared.ts @@ -0,0 +1,18 @@ +/** + * Shared prompt fragments used across all AI providers. + * Keep these provider-independent — no SDK-specific formatting here. + */ + +export const SYSTEM_CONTEXT = `You are RelWave AI, an expert database assistant embedded in the RelWave desktop application. +RelWave helps developers manage PostgreSQL, MySQL, MariaDB, and SQLite databases. +Always respond in clear, well-structured Markdown unless instructed otherwise. +Be concise, practical, and actionable. Avoid boilerplate preambles.`; + +export const MARKDOWN_INSTRUCTION = `Format your response in Markdown with: +- Level 2 headings (##) for major sections +- Bullet points for lists of items +- Code blocks (\`\`\`sql) for SQL examples +- Bold text for key terms and important warnings +Keep responses focused and under 1000 words unless the complexity demands more.`; + +export const NO_DATA_PROMPT = "No structured data was provided."; diff --git a/bridge/src/ai/providers/anthropic.provider.ts b/bridge/src/ai/providers/anthropic.provider.ts new file mode 100644 index 0000000..dac87c9 --- /dev/null +++ b/bridge/src/ai/providers/anthropic.provider.ts @@ -0,0 +1,65 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "claude-3-5-haiku-20241022"; + +export class AnthropicProvider implements AIProvider { + private client: Anthropic; + + constructor(apiKey: string) { + this.client = new Anthropic({ apiKey }); + } + + private async complete(system: string, user: string): Promise { + try { + const msg = await this.client.messages.create({ + model: DEFAULT_MODEL, + max_tokens: 4096, + system, + messages: [{ role: "user", content: user }], + }); + const block = msg.content[0]; + return block.type === "text" ? block.text : ""; + } catch (err) { + throw classifyError(err, "anthropic"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + await this.client.messages.create({ + model: DEFAULT_MODEL, + max_tokens: 10, + messages: [{ role: "user", content: "ping" }], + }); + return ""; + } catch (err) { + throw classifyError(err, "anthropic"); + } + } +} diff --git a/bridge/src/ai/providers/gemini.provider.ts b/bridge/src/ai/providers/gemini.provider.ts new file mode 100644 index 0000000..8de2bc8 --- /dev/null +++ b/bridge/src/ai/providers/gemini.provider.ts @@ -0,0 +1,61 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types/"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "gemini-1.5-flash"; + +export class GeminiProvider implements AIProvider { + private genAI: GoogleGenerativeAI; + + constructor(apiKey: string) { + this.genAI = new GoogleGenerativeAI(apiKey); + } + + private async complete(system: string, user: string): Promise { + try { + const model = this.genAI.getGenerativeModel({ + model: DEFAULT_MODEL, + systemInstruction: system, + generationConfig: { maxOutputTokens: 4096 }, + }); + const result = await model.generateContent(user); + return result.response.text(); + } catch (err) { + throw classifyError(err, "gemini"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + const model = this.genAI.getGenerativeModel({ model: DEFAULT_MODEL }); + await model.generateContent("ping"); + return ""; + } catch (err) { + throw classifyError(err, "gemini"); + } + } +} diff --git a/bridge/src/ai/providers/groq.provider.ts b/bridge/src/ai/providers/groq.provider.ts new file mode 100644 index 0000000..4b840d7 --- /dev/null +++ b/bridge/src/ai/providers/groq.provider.ts @@ -0,0 +1,66 @@ +import Groq from "groq-sdk"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types/"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "llama-3.3-70b-versatile"; + +export class GroqProvider implements AIProvider { + private client: Groq; + + constructor(apiKey: string) { + this.client = new Groq({ apiKey }); + } + + private async complete(system: string, user: string): Promise { + try { + const res = await this.client.chat.completions.create({ + model: DEFAULT_MODEL, + messages: [ + { role: "system", content: system }, + { role: "user", content: user }, + ], + max_tokens: 4096, + }); + return res.choices[0]?.message?.content ?? ""; + } catch (err) { + throw classifyError(err, "groq"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + await this.client.chat.completions.create({ + model: DEFAULT_MODEL, + messages: [{ role: "user", content: "ping" }], + max_tokens: 5, + }); + return ""; + } catch (err) { + throw classifyError(err, "groq"); + } + } +} diff --git a/bridge/src/ai/providers/mistral.provider.ts b/bridge/src/ai/providers/mistral.provider.ts new file mode 100644 index 0000000..abb74ee --- /dev/null +++ b/bridge/src/ai/providers/mistral.provider.ts @@ -0,0 +1,72 @@ +import { Mistral } from "@mistralai/mistralai"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types/"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "mistral-small-latest"; + +export class MistralProvider implements AIProvider { + private client: Mistral; + + constructor(apiKey: string) { + this.client = new Mistral({ apiKey }); + } + + private async complete(system: string, user: string): Promise { + try { + const res = await this.client.chat.complete({ + model: DEFAULT_MODEL, + messages: [ + { role: "system", content: system }, + { role: "user", content: user }, + ], + maxTokens: 4096, + }); + const choice = res.choices?.[0]; + const content = choice?.message?.content; + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content.map((c: any) => c.text ?? "").join(""); + } + return ""; + } catch (err) { + throw classifyError(err, "mistral"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + await this.client.chat.complete({ + model: DEFAULT_MODEL, + messages: [{ role: "user", content: "ping" }], + maxTokens: 5, + }); + return ""; + } catch (err) { + throw classifyError(err, "mistral"); + } + } +} diff --git a/bridge/src/ai/providers/ollama.provider.ts b/bridge/src/ai/providers/ollama.provider.ts new file mode 100644 index 0000000..5452c04 --- /dev/null +++ b/bridge/src/ai/providers/ollama.provider.ts @@ -0,0 +1,69 @@ +import { Ollama } from "ollama"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types/"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "llama3.2"; + +export class OllamaProvider implements AIProvider { + private client: Ollama; + private model: string; + + constructor(baseUrl?: string, model?: string) { + this.client = new Ollama({ host: baseUrl ?? "http://localhost:11434" }); + this.model = model?.trim() || DEFAULT_MODEL; + } + + private async complete(system: string, user: string): Promise { + try { + const res = await this.client.chat({ + model: this.model, + messages: [ + { role: "system", content: system }, + { role: "user", content: user }, + ], + options: { num_predict: 4096 }, + }); + return res.message?.content ?? ""; + } catch (err) { + throw classifyError(err, "ollama"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + // List models to verify Ollama is reachable and the model exists + const list = await this.client.list(); + const available = list.models.map((m: any) => m.name); + if (!available.some((n: string) => n.startsWith(this.model.split(":")[0]))) { + throw new Error(`Model "${this.model}" not found. Available: ${available.join(", ") || "none"}`); + } + return ""; + } catch (err) { + throw classifyError(err, "ollama"); + } + } +} diff --git a/bridge/src/ai/providers/openai.provider.ts b/bridge/src/ai/providers/openai.provider.ts new file mode 100644 index 0000000..1c9e841 --- /dev/null +++ b/bridge/src/ai/providers/openai.provider.ts @@ -0,0 +1,66 @@ +import OpenAI from "openai"; +import { AIProvider, classifyError } from "./types"; +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types"; +import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../prompts/query-explanation"; +import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation"; + +const DEFAULT_MODEL = "gpt-4o-mini"; + +export class OpenAIProvider implements AIProvider { + private client: OpenAI; + + constructor(apiKey: string) { + this.client = new OpenAI({ apiKey }); + } + + private async complete(system: string, user: string): Promise { + try { + const res = await this.client.chat.completions.create({ + model: DEFAULT_MODEL, + messages: [ + { role: "system", content: system }, + { role: "user", content: user }, + ], + max_tokens: 4096, + }); + return res.choices[0]?.message?.content ?? ""; + } catch (err) { + throw classifyError(err, "openai"); + } + } + + async analyzeSchema(input: SchemaAnalysisInput): Promise { + const { system, user } = buildSchemaAnalysisPrompt(input); + return this.complete(system, user); + } + + async explainQuery(input: QueryExplanationInput): Promise { + const { system, user } = buildQueryExplanationPrompt(input); + return this.complete(system, user); + } + + async recommendChart(input: ChartRecommendationInput): Promise { + const { system, user } = buildChartRecommendationPrompt(input); + const raw = await this.complete(system, user); + return parseChartRecommendation(raw); + } + + async testConnection(): Promise { + try { + await this.client.chat.completions.create({ + model: DEFAULT_MODEL, + messages: [{ role: "user", content: "ping" }], + max_tokens: 5, + }); + return ""; + } catch (err) { + throw classifyError(err, "openai"); + } + } +} diff --git a/bridge/src/ai/providers/types.ts b/bridge/src/ai/providers/types.ts new file mode 100644 index 0000000..2d4bdb4 --- /dev/null +++ b/bridge/src/ai/providers/types.ts @@ -0,0 +1,85 @@ +import { + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, + ChartRecommendation, +} from "../../types/ai"; + +// ── Provider interface ──────────────────────────────────────────────────── + +/** + * All AI providers must implement this interface. + * Frontend code never knows which concrete provider is active. + */ +export interface AIProvider { + /** Analyze a database schema and return a markdown report. */ + analyzeSchema(input: SchemaAnalysisInput): Promise; + + /** Explain a SQL query in plain language, returning markdown. */ + explainQuery(input: QueryExplanationInput): Promise; + + /** Recommend a chart type + axes for the given table. */ + recommendChart(input: ChartRecommendationInput): Promise; + + /** + * Verify that the provider is reachable with the supplied credentials. + * Resolves with an empty string on success, a user-facing message on failure. + */ + testConnection(): Promise; +} + +// ── Standardized error type ─────────────────────────────────────────────── + +export type AIErrorCode = + | "MISSING_API_KEY" + | "INVALID_API_KEY" + | "PROVIDER_UNAVAILABLE" + | "INVALID_MODEL" + | "NETWORK_FAILURE" + | "TIMEOUT" + | "RATE_LIMIT" + | "PARSE_ERROR" + | "UNKNOWN"; + +export class AIError extends Error { + readonly code: AIErrorCode; + readonly provider: string; + readonly originalMessage: string; + + constructor(code: AIErrorCode, provider: string, message: string) { + super(`[${provider}] ${message}`); + this.name = "AIError"; + this.code = code; + this.provider = provider; + this.originalMessage = message; + } +} + +/** + * Classify a raw SDK/network error into an AIErrorCode. + */ +export function classifyError(err: unknown, provider: string): AIError { + const msg = err instanceof Error ? err.message : String(err); + const lower = msg.toLowerCase(); + + if (lower.includes("api key") || lower.includes("apikey") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("401")) { + return new AIError("INVALID_API_KEY", provider, msg); + } + if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) { + return new AIError("RATE_LIMIT", provider, msg); + } + if (lower.includes("timeout") || lower.includes("timed out")) { + return new AIError("TIMEOUT", provider, msg); + } + if (lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("fetch failed") || lower.includes("network")) { + return new AIError("NETWORK_FAILURE", provider, msg); + } + if (lower.includes("model") && (lower.includes("not found") || lower.includes("invalid"))) { + return new AIError("INVALID_MODEL", provider, msg); + } + if (lower.includes("unavailable") || lower.includes("503") || lower.includes("overloaded")) { + return new AIError("PROVIDER_UNAVAILABLE", provider, msg); + } + + return new AIError("UNKNOWN", provider, msg); +} diff --git a/bridge/src/connectors/README.md b/bridge/src/connectors/README.md new file mode 100644 index 0000000..a94891f --- /dev/null +++ b/bridge/src/connectors/README.md @@ -0,0 +1,26 @@ +# Database Connectors (Bridge) + +This folder contains database-specific connector implementations used by the bridge to execute metadata, query, migration and CRUD operations. + +Layout +- `postgres.ts` - PostgreSQL connector, cache manager, metadata helpers, streaming query support and PostgreSQL-specific DDL/DML behavior. +- `mysql.ts` - MySQL connector for schema/table metadata, stats, migrations and query execution. +- `mariadb.ts` - MariaDB connector. Keep MariaDB behavior separate when it diverges from MySQL. +- `sqlite.ts` - SQLite connector using `better-sqlite3`, local file handling and SQLite-specific migration/query behavior. + +How it fits +- `src/services/queryExecutor.ts` routes bridge operations to the correct connector based on `DBType`. +- SQL strings should come from `src/queries/*` where practical, not be duplicated inline. +- Shared result shapes come from `src/types/common.ts`; database-specific config and metadata types live in `src/types/postgres.ts`, `src/types/mysql.ts` and `src/types/sqlite.ts`. +- Connection config objects are built by `src/services/connectionBuilder.ts`; connectors generally create their own driver client from that config for each operation. + +How to add connector behavior +1. Add database-specific SQL to `src/queries//` when the operation needs raw SQL. +2. Add or update typed method support in the relevant connector file. +3. Route the operation from `QueryExecutor` or the owning service. +4. Register any new public RPC method through a handler in `src/handlers`. + +Notes +- Keep connector methods focused on database interaction. Do not read UI payloads directly or handle RPC responses here. +- Invalidate or bypass connector caches when an operation mutates schema, table data or migration state. +- Prefer shared common types unless a database really needs extra metadata fields. diff --git a/bridge/src/connectors/mariadb.ts b/bridge/src/connectors/mariadb.ts index 8ce9151..6c77d95 100644 --- a/bridge/src/connectors/mariadb.ts +++ b/bridge/src/connectors/mariadb.ts @@ -7,7 +7,8 @@ import mysql, { import { loadLocalMigrations, writeBaselineMigration } from "../utils/baselineMigration"; import crypto from "crypto"; import fs from "fs"; -import { ensureDir, getMigrationsDir } from "../utils/config"; +import { ensureDir } from "../utils/config"; +import { projectStoreInstance } from "../services/projectStore"; import { CacheEntry, CACHE_TTL, @@ -34,6 +35,7 @@ import { MySQLAlterTableOperation as MariaDBAlterTableOperation, MySQLDropMode as MariaDBDropMode, } from "../types/mysql"; +import { SchemaFile } from "../services/projectStore"; export type { ColumnDetail, @@ -548,7 +550,7 @@ export function streamQueryCancelable( conn = await pool.getConnection(); const [pidRows] = await conn.execute(GET_CONNECTION_ID); - backendPid = pidRows[0].pid; + backendPid = (pidRows as any[])[0].pid; const raw = (conn as any).connection; query = raw.query(sql); @@ -916,7 +918,12 @@ export async function getSchemaMetadataBatch( not_nullable: Boolean(row.not_nullable), default_value: row.default_value, is_primary_key: Boolean(row.is_primary_key), - is_foreign_key: Boolean(row.is_foreign_key) + is_foreign_key: Boolean(row.is_foreign_key), + is_unique: Boolean(row.is_unique), + is_serial: Boolean(row.is_serial), + comment: row.comment, + check_constraint: row.check_constraint, + ordinal_position: row.ordinal_position }); } @@ -1349,10 +1356,10 @@ export async function insertBaseline( await connection.query(INSERT_MIGRATION, [version, name, checksum]); } - export async function baselineIfNeeded( conn: MariaDBConfig, - migrationsDir: string + migrationsDir: string, + snapshot?: SchemaFile ) { try { await ensureMigrationTable(conn); @@ -1363,10 +1370,22 @@ export async function baselineIfNeeded( const version = Date.now().toString(); const name = "baseline_existing_schema"; + const fakeSnapshot = snapshot || { + version: 2, + projectId: "", + databaseId: "", + dialect: "mysql", + schemas: [], + cachedAt: "", + relwaveVersion: "", + schemaHash: "" + }; + const filePath = writeBaselineMigration( migrationsDir, version, - name + name, + fakeSnapshot ); const checksum = crypto @@ -1420,11 +1439,19 @@ export async function connectToDatabase( options?: { readOnly?: boolean } ) { let baselineResult = { baselined: false }; - const migrationsDir = getMigrationsDir(connectionId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(connectionId); ensureDir(migrationsDir); // 1️⃣ Baseline (ONLY if not read-only) if (!options?.readOnly) { - baselineResult = await baselineIfNeeded(cfg, migrationsDir); + // Pass real schema snapshot so baseline contains actual DDL + let snapshot: any = undefined; + try { + const project = await projectStoreInstance.getProjectByDatabaseId(connectionId); + if (project) { + snapshot = await projectStoreInstance.getSchema(project.id) || undefined; + } + } catch { } + baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot); } // 2️⃣ Load schema (read-only introspection) diff --git a/bridge/src/connectors/mysql.ts b/bridge/src/connectors/mysql.ts index 72acba9..dd49a42 100644 --- a/bridge/src/connectors/mysql.ts +++ b/bridge/src/connectors/mysql.ts @@ -7,7 +7,8 @@ import mysql, { import { loadLocalMigrations, writeBaselineMigration } from "../utils/baselineMigration"; import crypto from "crypto"; import fs from "fs"; -import { ensureDir, getMigrationsDir } from "../utils/config"; +import { ensureDir } from "../utils/config"; +import { projectStoreInstance } from "../services/projectStore"; import { CacheEntry, CACHE_TTL, @@ -34,6 +35,7 @@ import { MySQLAlterTableOperation, MySQLDropMode, } from "../types/mysql"; +import { SchemaFile } from "../services/projectStore"; // Re-export types for backward compatibility export type { @@ -523,7 +525,7 @@ export function streamQueryCancelable( conn = await pool.getConnection(); const [pidRows] = await conn.execute(GET_CONNECTION_ID); - backendPid = pidRows[0].pid; + backendPid = (pidRows as any[])[0].pid; const raw = (conn as any).connection; query = raw.query(sql); @@ -892,7 +894,12 @@ export async function getSchemaMetadataBatch( not_nullable: Boolean(row.not_nullable), default_value: row.default_value, is_primary_key: Boolean(row.is_primary_key), - is_foreign_key: Boolean(row.is_foreign_key) + is_foreign_key: Boolean(row.is_foreign_key), + is_unique: Boolean(row.is_unique), + is_serial: Boolean(row.is_serial), + comment: row.comment, + check_constraint: row.check_constraint, + ordinal_position: row.ordinal_position }); } @@ -1103,7 +1110,7 @@ function groupMySQLIndexes(indexes: IndexInfo[]) { } return [...map.values()].map(group => - group.sort((a, b) => a.seq_in_index - b.seq_in_index) + group.sort((a, b) => (a.seq_in_index ?? 0) - (b.seq_in_index ?? 0)) ); } @@ -1324,10 +1331,10 @@ export async function insertBaseline( await connection.query(INSERT_MIGRATION, [version, name, checksum]); } - export async function baselineIfNeeded( conn: MySQLConfig, - migrationsDir: string + migrationsDir: string, + snapshot?: SchemaFile ) { try { await ensureMigrationTable(conn); @@ -1338,10 +1345,22 @@ export async function baselineIfNeeded( const version = Date.now().toString(); const name = "baseline_existing_schema"; + const fakeSnapshot = snapshot || { + version: 2, + projectId: "", + databaseId: "", + dialect: "mysql", + schemas: [], + cachedAt: "", + relwaveVersion: "", + schemaHash: "" + }; + const filePath = writeBaselineMigration( migrationsDir, version, - name + name, + fakeSnapshot ); const checksum = crypto @@ -1395,11 +1414,19 @@ export async function connectToDatabase( options?: { readOnly?: boolean } ) { let baselineResult = { baselined: false }; - const migrationsDir = getMigrationsDir(connectionId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(connectionId); ensureDir(migrationsDir); // 1️⃣ Baseline (ONLY if not read-only) if (!options?.readOnly) { - baselineResult = await baselineIfNeeded(cfg, migrationsDir); + // Pass real schema snapshot so baseline contains actual DDL + let snapshot: any = undefined; + try { + const project = await projectStoreInstance.getProjectByDatabaseId(connectionId); + if (project) { + snapshot = await projectStoreInstance.getSchema(project.id) || undefined; + } + } catch {} + baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot); } // 2️⃣ Load schema (read-only introspection) diff --git a/bridge/src/connectors/postgres.ts b/bridge/src/connectors/postgres.ts index 19084dc..4f56d94 100644 --- a/bridge/src/connectors/postgres.ts +++ b/bridge/src/connectors/postgres.ts @@ -5,7 +5,8 @@ import { Readable } from "stream"; import { loadLocalMigrations, writeBaselineMigration } from "../utils/baselineMigration"; import crypto from "crypto"; import fs from "fs"; -import { ensureDir, getMigrationsDir } from "../utils/config"; +import { ensureDir } from "../utils/config"; +import { projectStoreInstance } from "../services/projectStore"; import { CacheEntry, CACHE_TTL, @@ -32,6 +33,7 @@ import { PGAlterTableOperation, PGDropMode, } from "../types/postgres"; +import { SchemaFile } from "../services/projectStore"; export type { PGConfig, @@ -801,7 +803,12 @@ export async function getSchemaMetadataBatch( not_nullable: row.not_nullable, default_value: row.default_value, is_primary_key: row.is_primary_key, - is_foreign_key: row.is_foreign_key + is_foreign_key: row.is_foreign_key, + is_unique: row.is_unique, + is_serial: row.is_serial, + check_constraint: row.check_constraint, + comment: row.comment, + ordinal_position: row.ordinal_position }); } @@ -1169,7 +1176,7 @@ function groupIndexes(indexes: IndexInfo[]) { } return [...map.values()].map(group => - group.sort((a, b) => a.ordinal_position - b.ordinal_position) + group.sort((a, b) => (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0)) ); } @@ -1409,10 +1416,10 @@ export async function insertBaseline( } } - export async function baselineIfNeeded( conn: PGConfig, - migrationsDir: string + migrationsDir: string, + snapshot?: SchemaFile ) { const client = createClient(conn); @@ -1426,10 +1433,22 @@ export async function baselineIfNeeded( const version = Date.now().toString(); const name = "baseline_existing_schema"; + const fakeSnapshot = snapshot || { + version: 2, + projectId: "", + databaseId: "", + dialect: "postgresql", + schemas: [], + cachedAt: "", + relwaveVersion: "", + schemaHash: "" + }; + const filePath = writeBaselineMigration( migrationsDir, version, - name + name, + fakeSnapshot ); const checksum = crypto @@ -1485,11 +1504,19 @@ export async function connectToDatabase( ) { // 1️⃣ Baseline (only if allowed) let baselineResult = { baselined: false }; - const migrationsDir = getMigrationsDir(connectionId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(connectionId); ensureDir(migrationsDir); if (!options?.readOnly) { - baselineResult = await baselineIfNeeded(cfg, migrationsDir); + // Pass real schema snapshot so baseline contains actual DDL + let snapshot: any = undefined; + try { + const project = await projectStoreInstance.getProjectByDatabaseId(connectionId); + if (project) { + snapshot = await projectStoreInstance.getSchema(project.id) || undefined; + } + } catch { } + baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot); } // 2️⃣ Load schema (read-only) diff --git a/bridge/src/connectors/sqlite.ts b/bridge/src/connectors/sqlite.ts index 0815fb0..38b56e4 100644 --- a/bridge/src/connectors/sqlite.ts +++ b/bridge/src/connectors/sqlite.ts @@ -5,7 +5,8 @@ import crypto from "crypto"; import fs from "fs"; import os from "os"; import path from "path"; -import { ensureDir, getMigrationsDir } from "../utils/config"; +import { ensureDir } from "../utils/config"; +import { projectStoreInstance } from "../services/projectStore"; import { isWindowsDriveRootPath, normalizeSQLitePath } from "../utils/sqlitePath"; import { CacheEntry, @@ -55,6 +56,7 @@ import { } from "../queries/sqlite/migrations"; import { SQLITE_GET_TABLE_SQL } from "../queries/sqlite/constraints"; import { sqliteQuoteIdentifier } from "../queries/sqlite/crud"; +import { SchemaFile } from "../services/projectStore"; // ============================================ // CACHING SYSTEM FOR SQLITE CONNECTOR @@ -322,7 +324,7 @@ function getInstalledBindingCandidates(execDir: string): string[] { return Array.from(searchRoots, (root) => path.join(root, "better_sqlite3.node")); } -function resolvePkgNativeBindingPath(): string | undefined { +export function resolvePkgNativeBindingPath(): string | undefined { if (cachedNativeBindingPath !== undefined) { return cachedNativeBindingPath ?? undefined; } @@ -376,7 +378,9 @@ function resolvePkgNativeBindingPath(): string | undefined { const snapshotRoot = path.join(path.sep, "snapshot", "bridge", "node_modules"); snapshotCandidates.push( path.join(snapshotRoot, "better-sqlite3", "build", "Release", "better_sqlite3.node"), - path.join(snapshotRoot, ".pnpm", "better-sqlite3@12.6.2", "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node") + path.join(snapshotRoot, ".pnpm", "better-sqlite3@12.6.2", "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node"), + path.join(snapshotRoot, ".pnpm", "better-sqlite3@11.10.0", "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node"), + path.join(snapshotRoot, ".pnpm", "better-sqlite3@11.9.0", "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node") ); const targetNodeFile = path.join(os.tmpdir(), `relwave-better_sqlite3-${process.pid}.node`); @@ -732,6 +736,11 @@ export async function getSchemaMetadataBatch( default_value: col.dflt_value, is_primary_key: col.pk > 0, is_foreign_key: fkColumns.has(col.name), + is_unique: false, // will be updated below if unique index exists + is_serial: col.pk > 0 && (col.type || '').toLowerCase() === 'integer', + check_constraint: undefined, + comment: undefined, + ordinal_position: col.cid + 1, })); const primaryKeys: PrimaryKeyInfo[] = cols @@ -769,14 +778,19 @@ export async function getSchemaMetadataBatch( ordinal_position: col.seqno, }); - if (idx.unique === 1 && idx.origin !== 'pk') { - uniqueConstraints.push({ - constraint_name: idx.name, - table_schema: 'main', - table_name: tableName, - column_name: col.name, - ordinal_position: col.seqno, - }); + if (idx.unique === 1) { + const c = columns.find(c => c.name === col.name); + if (c) c.is_unique = true; + + if (idx.origin !== 'pk') { + uniqueConstraints.push({ + constraint_name: idx.name, + table_schema: 'main', + table_name: tableName, + column_name: col.name, + ordinal_position: col.seqno, + }); + } } } } @@ -1097,7 +1111,8 @@ export async function insertBaseline( /** Baseline if needed */ export async function baselineIfNeeded( cfg: SQLiteConfig, - migrationsDir: string + migrationsDir: string, + snapshot?: SchemaFile ) { await ensureMigrationTable(cfg); @@ -1107,7 +1122,18 @@ export async function baselineIfNeeded( const version = Date.now().toString(); const name = "baseline_existing_schema"; - const filePath = writeBaselineMigration(migrationsDir, version, name); + const fakeSnapshot = snapshot || { + version: 2, + projectId: "", + databaseId: "", + dialect: "sqlite", + schemas: [], + cachedAt: "", + relwaveVersion: "", + schemaHash: "" + }; + + const filePath = writeBaselineMigration(migrationsDir, version, name, fakeSnapshot); const checksum = crypto .createHash("sha256") @@ -1142,11 +1168,19 @@ export async function connectToDatabase( options?: { readOnly?: boolean } ) { let baselineResult = { baselined: false }; - const migrationsDir = getMigrationsDir(connectionId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(connectionId); ensureDir(migrationsDir); if (!options?.readOnly) { - baselineResult = await baselineIfNeeded(cfg, migrationsDir); + // Pass real schema snapshot so baseline contains actual DDL + let snapshot: any = undefined; + try { + const project = await projectStoreInstance.getProjectByDatabaseId(connectionId); + if (project) { + snapshot = await projectStoreInstance.getSchema(project.id) || undefined; + } + } catch {} + baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot); } const schema = await listSchemas(cfg); diff --git a/bridge/src/handlers/README.md b/bridge/src/handlers/README.md new file mode 100644 index 0000000..110354b --- /dev/null +++ b/bridge/src/handlers/README.md @@ -0,0 +1,33 @@ +# RPC Handlers (Bridge) + +This folder contains JSON-RPC handler classes. Handlers translate frontend RPC calls into service/query work and send normalized RPC responses or errors. + +Layout +- `databaseHandlers.ts` - database connection CRUD, connection tests and schema/table metadata entry points. +- `queryHandlers.ts` - query execution, table browsing and table/row mutation handlers. +- `sessionHandlers.ts` - query session lifecycle and cancellation. +- `statsHandlers.ts` - database and aggregate statistics. +- `migrationHandlers.ts` - migration generation, application, rollback, deletion and SQL retrieval. +- `projectHandlers.ts` - project persistence, schema snapshots, ER diagrams, annotations, saved queries and import/export. +- `gitHandlers.ts` - core local Git actions. +- `gitAdvancedHandlers.ts` - remotes, push/pull/fetch and revert operations. +- `monitoringHandlers.ts` - database monitoring snapshot and websocket info endpoints. +- `aiHandlers.ts` - AI provider tests, schema analysis, query explanation, chart recommendation and history. + +How it fits +- `src/jsonRpcHandler.ts` constructs handlers and registers public method names such as `db.list`, `query.run`, `project.create`, `git.status` and `ai.analyzeSchema`. +- Handlers depend on services like `DatabaseService`, `QueryExecutor`, `MonitoringService` and `GitService`. +- Handlers own RPC validation and error mapping. Services and connectors should throw normal errors rather than calling `rpc.sendResponse` directly. + +How to add an RPC method +1. Add a handler method in the correct handler class. +2. Validate required params near the top of the handler. +3. Call service/query logic and return `{ ok: true, data }` through `rpc.sendResponse`. +4. Convert failures to `rpc.sendError(id, { code, message })`. +5. Register the method in `src/jsonRpcHandler.ts`. +6. Add or update the matching frontend bridge service in `src/services/bridge`. + +Notes +- Keep handler classes thin. If logic grows beyond request validation and orchestration, move it into `src/services`. +- Avoid leaking credentials in responses; strip or omit password and credential identifiers before responding. +- Use stable error codes because the frontend may surface them in notifications. diff --git a/bridge/src/handlers/aiHandlers.ts b/bridge/src/handlers/aiHandlers.ts new file mode 100644 index 0000000..49201b4 --- /dev/null +++ b/bridge/src/handlers/aiHandlers.ts @@ -0,0 +1,222 @@ +import { Logger } from "pino"; +import { Rpc } from "../types"; +import { AIService } from "../services/aiService"; +import { AIError } from "../ai/providers/types"; +import { + AIAnalyzeSchemaParams, + AIExplainQueryParams, + AIRecommendChartParams, + AITestConnectionParams, +} from "../types/ai"; +import { + getOrCall, + hashSchemaAnalysis, + hashQueryExplanation, + hashChartRecommendation, +} from "../services/aiCacheService"; +import { aiHistoryStore } from "../services/aiHistoryStore"; +import { buildSchemaAnalysisPrompt } from "../ai/prompts/schema-analysis"; +import { buildQueryExplanationPrompt } from "../ai/prompts/query-explanation"; +import { buildChartRecommendationPrompt } from "../ai/prompts/chart-recommendation"; +import { parseChartRecommendation } from "../ai/prompts/chart-recommendation"; + +export class AIHandlers { + private aiService: AIService; + + constructor(private rpc: Rpc, private logger: Logger) { + this.aiService = new AIService(); + } + + async handleTestConnection(params: AITestConnectionParams, id: number | string) { + try { + const provider = this.aiService.resolveProvider(params.settings); + await provider.testConnection(); + this.rpc.sendResponse(id, { ok: true, data: { connected: true } }); + } catch (err) { + this.logger?.warn({ err }, "ai.testConnection failed"); + const msg = err instanceof AIError ? err.originalMessage : String(err); + const code = err instanceof AIError ? err.code : "UNKNOWN"; + this.rpc.sendError(id, { code, message: msg }); + } + } + + async handleAnalyzeSchema(params: AIAnalyzeSchemaParams & { skipCache?: boolean }, id: number | string) { + try { + if (!params?.input?.tables?.length) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "No tables provided." }); + } + + const hash = hashSchemaAnalysis(params.input, params.datasourceName); + const { system, user } = buildSchemaAnalysisPrompt(params.input); + const prompt = `${system}\n\n${user}`; + + const result = await getOrCall({ + feature: "schema-analysis", + hash, + settings: params.settings, + prompt, + callFn: async () => { + const provider = this.aiService.resolveProvider(params.settings); + return provider.analyzeSchema(params.input); + }, + meta: { datasource_id: params.datasourceName, table_name: params.tableName }, + skipCache: params.skipCache, + }); + + this.rpc.sendResponse(id, { + ok: true, + data: { + markdown: result.response, + cached: result.cached, + createdAt: result.createdAt, + }, + }); + } catch (err) { + this.logger?.error({ err }, "ai.analyzeSchema failed"); + const msg = err instanceof AIError ? err.originalMessage : String(err); + const code = err instanceof AIError ? err.code : "UNKNOWN"; + this.rpc.sendError(id, { code, message: msg }); + } + } + + async handleExplainQuery(params: AIExplainQueryParams & { skipCache?: boolean }, id: number | string) { + try { + if (!params?.input?.sql?.trim()) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "No SQL provided." }); + } + + const hash = hashQueryExplanation(params.input, params.datasourceName); + const { system, user } = buildQueryExplanationPrompt(params.input); + const prompt = `${system}\n\n${user}`; + + const result = await getOrCall({ + feature: "query-explanation", + hash, + settings: params.settings, + prompt, + callFn: async () => { + const provider = this.aiService.resolveProvider(params.settings); + return provider.explainQuery(params.input); + }, + meta: { datasource_id: params.datasourceName, table_name: params.tableName }, + skipCache: params.skipCache, + }); + + this.rpc.sendResponse(id, { + ok: true, + data: { + markdown: result.response, + cached: result.cached, + createdAt: result.createdAt, + }, + }); + } catch (err) { + this.logger?.error({ err }, "ai.explainQuery failed"); + const msg = err instanceof AIError ? err.originalMessage : String(err); + const code = err instanceof AIError ? err.code : "UNKNOWN"; + this.rpc.sendError(id, { code, message: msg }); + } + } + + async handleRecommendChart(params: AIRecommendChartParams & { skipCache?: boolean }, id: number | string) { + try { + if (!params?.input?.tableName || !params?.input?.columns?.length) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing tableName or columns." }); + } + + const hash = hashChartRecommendation(params.input, params.datasourceName); + const { system, user } = buildChartRecommendationPrompt(params.input); + const prompt = `${system}\n\n${user}`; + + const result = await getOrCall({ + feature: "chart-recommendation", + hash, + settings: params.settings, + prompt, + callFn: async () => { + const provider = this.aiService.resolveProvider(params.settings); + const rec = await provider.recommendChart(params.input); + // Serialize the recommendation as JSON so it can be stored as a string + return JSON.stringify(rec); + }, + meta: { + datasource_id: params.datasourceName, + table_name: params.tableName ?? params.input.tableName, + }, + skipCache: params.skipCache, + }); + + // Parse the stored JSON response back into a ChartRecommendation + let recommendation; + try { + recommendation = JSON.parse(result.response); + } catch { + recommendation = parseChartRecommendation(result.response); + } + + this.rpc.sendResponse(id, { + ok: true, + data: { + ...recommendation, + cached: result.cached, + createdAt: result.createdAt, + }, + }); + } catch (err) { + this.logger?.error({ err }, "ai.recommendChart failed"); + const msg = err instanceof AIError ? err.originalMessage : String(err); + const code = err instanceof AIError ? err.code : "UNKNOWN"; + this.rpc.sendError(id, { code, message: msg }); + } + } + + // ── History CRUD handlers ───────────────────────────────────────────── + + async handleGetHistory(params: { feature?: string; provider?: string; limit?: number; offset?: number }, id: number | string) { + try { + const result = aiHistoryStore.list({ + feature: params?.feature, + provider: params?.provider, + limit: params?.limit, + offset: params?.offset, + }); + this.rpc.sendResponse(id, { ok: true, data: result }); + } catch (err: any) { + this.logger?.error({ err }, "ai.getHistory failed"); + this.rpc.sendError(id, { code: "HISTORY_ERROR", message: err?.message ?? String(err) }); + } + } + + async handleGetHistoryById(params: { id: number }, id: number | string) { + try { + const record = aiHistoryStore.getById(params.id); + if (!record) { + return this.rpc.sendError(id, { code: "NOT_FOUND", message: "History entry not found." }); + } + this.rpc.sendResponse(id, { ok: true, data: record }); + } catch (err: any) { + this.logger?.error({ err }, "ai.getHistoryById failed"); + this.rpc.sendError(id, { code: "HISTORY_ERROR", message: err?.message ?? String(err) }); + } + } + + async handleDeleteHistory(params: { id: number }, id: number | string) { + try { + const deleted = aiHistoryStore.deleteById(params.id); + this.rpc.sendResponse(id, { ok: true, data: { deleted } }); + } catch (err: any) { + this.logger?.error({ err }, "ai.deleteHistory failed"); + this.rpc.sendError(id, { code: "HISTORY_ERROR", message: err?.message ?? String(err) }); + } + } + + async handleClearHistory(_params: unknown, id: number | string) { + try { + const count = aiHistoryStore.clearAll(); + this.rpc.sendResponse(id, { ok: true, data: { deletedCount: count } }); + } catch (err: any) { + this.logger?.error({ err }, "ai.clearHistory failed"); + this.rpc.sendError(id, { code: "HISTORY_ERROR", message: err?.message ?? String(err) }); + } + } +} diff --git a/bridge/src/handlers/migrationHandlers.ts b/bridge/src/handlers/migrationHandlers.ts index f4cefd8..9cbdcbb 100644 --- a/bridge/src/handlers/migrationHandlers.ts +++ b/bridge/src/handlers/migrationHandlers.ts @@ -2,9 +2,9 @@ import { Rpc } from "../types"; import { DatabaseService } from "../services/databaseService"; import { QueryExecutor } from "../services/queryExecutor"; import { Logger } from "pino"; -import { getMigrationsDir } from "../utils/config"; import path from "path"; import fs from "fs"; +import { projectStoreInstance } from "../services/projectStore"; export class MigrationHandlers { constructor( @@ -26,7 +26,7 @@ export class MigrationHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Generate migration file const { generateCreateTableMigration, writeMigrationFile } = await import('../utils/migrationGenerator'); @@ -66,7 +66,7 @@ export class MigrationHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Generate migration file const { generateAlterTableMigration, writeMigrationFile } = await import('../utils/migrationGenerator'); @@ -105,7 +105,7 @@ export class MigrationHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Generate migration file const { generateDropTableMigration, writeMigrationFile } = await import('../utils/migrationGenerator'); @@ -144,7 +144,7 @@ export class MigrationHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Find migration file const { listMigrationFiles } = await import('../utils/migrationFileReader'); @@ -160,7 +160,6 @@ export class MigrationHandlers { const migrationFilePath = path.join(migrationsDir, migrationFile); - // Apply migration if (dbType === "mysql") { await this.queryExecutor.mysql.applyMigration(conn, migrationFilePath); } else if (dbType === "postgres") { @@ -171,6 +170,44 @@ export class MigrationHandlers { await this.queryExecutor.sqlite.applyMigration(conn, migrationFilePath); } + // Hook syncMigrationFiles + try { + const { projectStoreInstance } = await import("../services/projectStore"); + const { gitServiceInstance } = await import("../services/gitService"); + const { writeMigrationLock } = await import("../services/migrationLock"); + + const project = await projectStoreInstance.getProjectByDatabaseId(dbId); + if (project) { + // Update Migration Lock first so it gets included in the git sync commit + let appliedMigrations: any[] = []; + if (dbType === "mysql") { + appliedMigrations = await require("../connectors/mysql").listAppliedMigrations(conn); + } else if (dbType === "postgres") { + appliedMigrations = await require("../connectors/postgres").listAppliedMigrations(conn); + } else if (dbType === "mariadb") { + appliedMigrations = await require("../connectors/mariadb").listAppliedMigrations(conn); + } else if (dbType === "sqlite") { + appliedMigrations = await require("../connectors/sqlite").listAppliedMigrations(conn); + } + + const schemaFile = await projectStoreInstance.getSchema(project.id); + const schemaHash = (schemaFile as any)?.schemaHash || ""; + + const appliedVersions = appliedMigrations.map(m => m.version); + await writeMigrationLock(dbId, schemaHash, appliedVersions); + + const projectDir = await projectStoreInstance.resolveProjectDir(project.id); + if (projectDir) { + const syncResult = await gitServiceInstance.syncMigrationFiles(projectDir); + if (syncResult.error) { + this.logger?.warn({ error: syncResult.error }, "Git sync failed after migration apply"); + } + } + } + } catch (syncErr) { + this.logger?.error({ err: syncErr }, "syncMigrationFiles/lock hook failed"); + } + this.rpc.sendResponse(id, { ok: true }); } catch (e: any) { this.logger?.error({ e }, "migration.apply failed"); @@ -178,6 +215,132 @@ export class MigrationHandlers { } } + async handleApplyMigrations(params: any, id: number | string) { + try { + let { dbId, projectId } = params || {}; + + // Resolve dbId from projectId if not directly provided + if (!dbId && projectId) { + const project = await projectStoreInstance.getProject(projectId); + if (!project?.databaseId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Project has no linked database" }); + } + dbId = project.databaseId; + } + if (!dbId) return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing dbId or projectId" }); + + const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); + + const { loadLocalMigrations } = await import('../utils/baselineMigration'); + const localMigrations = await loadLocalMigrations(migrationsDir); + + let connector: any; + if (dbType === "mysql") connector = require("../connectors/mysql"); + else if (dbType === "postgres") connector = require("../connectors/postgres"); + else if (dbType === "mariadb") connector = require("../connectors/mariadb"); + else if (dbType === "sqlite") connector = require("../connectors/sqlite"); + + const appliedMigrations = await connector.listAppliedMigrations(conn); + const appliedSet = new Set(appliedMigrations.map((m: any) => m.version)); + + const pending = localMigrations.filter(m => !appliedSet.has(m.version)).sort((a, b) => a.version.localeCompare(b.version)); + + for (const migration of pending) { + const migrationFile = (await fs.promises.readdir(migrationsDir)).find(f => f.startsWith(migration.version)); + if (!migrationFile) throw new Error(`Migration file not found for version: ${migration.version}`); + + const migrationFilePath = path.join(migrationsDir, migrationFile); + if (dbType === "mysql") await this.queryExecutor.mysql.applyMigration(conn, migrationFilePath); + else if (dbType === "postgres") await this.queryExecutor.postgres.applyMigration(conn, migrationFilePath); + else if (dbType === "mariadb") await this.queryExecutor.mariadb.applyMigration(conn, migrationFilePath); + else if (dbType === "sqlite") await this.queryExecutor.sqlite.applyMigration(conn, migrationFilePath); + } + + try { + const { projectStoreInstance } = await import("../services/projectStore"); + const { writeMigrationLock } = await import("../services/migrationLock"); + const project = await projectStoreInstance.getProjectByDatabaseId(dbId); + if (project) { + const schemaFile = await projectStoreInstance.getSchema(project.id); + const schemaHash = (schemaFile as any)?.schemaHash || ""; + const updatedApplied = await connector.listAppliedMigrations(conn); + await writeMigrationLock(dbId, schemaHash, updatedApplied.map((m: any) => m.version)); + } + } catch (syncErr) { + this.logger?.error({ err: syncErr }, "Lock hook failed"); + } + + this.rpc.sendResponse(id, { ok: true, count: pending.length }); + } catch (e: any) { + this.logger?.error({ e }, "migration.applyMigrations failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleApplySnapshot(params: any, id: number | string) { + try { + let { dbId, projectId } = params || {}; + const { projectStoreInstance } = await import("../services/projectStore"); + + // Resolve dbId from projectId if not directly provided + if (!dbId && projectId) { + const project = await projectStoreInstance.getProject(projectId); + if (!project?.databaseId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Project has no linked database" }); + } + dbId = project.databaseId; + } + if (!dbId) return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing dbId or projectId" }); + + const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); + + const project = await projectStoreInstance.getProjectByDatabaseId(dbId); + if (!project) throw new Error("Project not found for database"); + const snapshot = await projectStoreInstance.getSchema(project.id); + if (!snapshot) throw new Error("Schema snapshot not found"); + + const { generateBaselineSQL } = await import("../utils/baselineMigration"); + const baselineSQL = generateBaselineSQL(snapshot, Date.now().toString(), "apply_snapshot"); + + let connector: any; + if (dbType === "mysql") connector = require("../connectors/mysql"); + else if (dbType === "postgres") connector = require("../connectors/postgres"); + else if (dbType === "mariadb") connector = require("../connectors/mariadb"); + else if (dbType === "sqlite") connector = require("../connectors/sqlite"); + + // For full snapshot apply, we assume DB is empty or we should drop it. + // A full implementation would drop tables here. + // For now, we write the SQL and apply it. + + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); + const versionStr = Date.now().toString(); + const baselinePath = path.join(migrationsDir, `${versionStr}_apply_snapshot.sql`); + fs.writeFileSync(baselinePath, baselineSQL, "utf8"); + + if (dbType === "mysql") await this.queryExecutor.mysql.applyMigration(conn, baselinePath); + else if (dbType === "postgres") await this.queryExecutor.postgres.applyMigration(conn, baselinePath); + else if (dbType === "mariadb") await this.queryExecutor.mariadb.applyMigration(conn, baselinePath); + else if (dbType === "sqlite") await this.queryExecutor.sqlite.applyMigration(conn, baselinePath); + + if (fs.existsSync(baselinePath)) fs.unlinkSync(baselinePath); + + try { + const { writeMigrationLock } = await import("../services/migrationLock"); + const schemaHash = (snapshot as any)?.schemaHash || ""; + const updatedApplied = await connector.listAppliedMigrations(conn); + await writeMigrationLock(dbId, schemaHash, updatedApplied.map((m: any) => m.version)); + } catch (syncErr) { + this.logger?.error({ err: syncErr }, "Lock hook failed"); + } + + this.rpc.sendResponse(id, { ok: true }); + } catch (e: any) { + this.logger?.error({ e }, "migration.applySnapshot failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + async handleRollbackMigration(params: any, id: number | string) { try { const { dbId, version } = params || {}; @@ -190,7 +353,7 @@ export class MigrationHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Find migration file const { listMigrationFiles } = await import('../utils/migrationFileReader'); @@ -217,6 +380,44 @@ export class MigrationHandlers { await this.queryExecutor.sqlite.rollbackMigration(conn, version, migrationFilePath); } + // Hook syncMigrationFiles and Lock Update + try { + const { projectStoreInstance } = await import("../services/projectStore"); + const { gitServiceInstance } = await import("../services/gitService"); + const { writeMigrationLock } = await import("../services/migrationLock"); + + const project = await projectStoreInstance.getProjectByDatabaseId(dbId); + if (project) { + // Update Migration Lock first so it gets included in the git sync commit + let appliedMigrations: any[] = []; + if (dbType === "mysql") { + appliedMigrations = await require("../connectors/mysql").listAppliedMigrations(conn); + } else if (dbType === "postgres") { + appliedMigrations = await require("../connectors/postgres").listAppliedMigrations(conn); + } else if (dbType === "mariadb") { + appliedMigrations = await require("../connectors/mariadb").listAppliedMigrations(conn); + } else if (dbType === "sqlite") { + appliedMigrations = await require("../connectors/sqlite").listAppliedMigrations(conn); + } + + const schemaFile = await projectStoreInstance.getSchema(project.id); + const schemaHash = (schemaFile as any)?.schemaHash || ""; + + const appliedVersions = appliedMigrations.map(m => m.version); + await writeMigrationLock(dbId, schemaHash, appliedVersions); + + const projectDir = await projectStoreInstance.resolveProjectDir(project.id); + if (projectDir) { + const syncResult = await gitServiceInstance.syncMigrationFiles(projectDir); + if (syncResult.error) { + this.logger?.warn({ error: syncResult.error }, "Git sync failed after migration rollback"); + } + } + } + } catch (syncErr) { + this.logger?.error({ err: syncErr }, "syncMigrationFiles/lock hook failed"); + } + this.rpc.sendResponse(id, { ok: true }); } catch (e: any) { this.logger?.error({ e }, "migration.rollback failed"); @@ -235,7 +436,7 @@ export class MigrationHandlers { }); } - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Find and delete migration file const { listMigrationFiles } = await import('../utils/migrationFileReader'); @@ -270,7 +471,7 @@ export class MigrationHandlers { }); } - const migrationsDir = getMigrationsDir(dbId); + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); // Find and read migration file const { listMigrationFiles, readMigrationFile } = await import('../utils/migrationFileReader'); diff --git a/bridge/src/handlers/projectHandlers.ts b/bridge/src/handlers/projectHandlers.ts index d9f3f22..c62600c 100644 --- a/bridge/src/handlers/projectHandlers.ts +++ b/bridge/src/handlers/projectHandlers.ts @@ -1,6 +1,10 @@ import { Rpc } from "../types"; import { Logger } from "pino"; import { projectStoreInstance } from "../services/projectStore"; +import { DatabaseService } from "../services/databaseService"; +import { QueryExecutor } from "../services/queryExecutor"; +import { gitServiceInstance } from "../services/gitService"; +import path from "path"; /** * RPC handlers for project CRUD and sub-resource operations. @@ -9,7 +13,9 @@ import { projectStoreInstance } from "../services/projectStore"; export class ProjectHandlers { constructor( private rpc: Rpc, - private logger: Logger + private logger: Logger, + private dbService: DatabaseService, + private queryExecutor: QueryExecutor ) { } @@ -170,6 +176,67 @@ export class ProjectHandlers { } } + async handleRefreshSchemaCache(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + } + + const project = await projectStoreInstance.getProject(projectId); + if (!project) { + return this.rpc.sendError(id, { code: "NOT_FOUND", message: "Project not found" }); + } + + if (!project.databaseId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Project has no database ID" }); + } + + // Get DB connection + const { conn, dbType } = await this.dbService.getDatabaseConnection(project.databaseId); + const dbSchema = await this.queryExecutor.listSchemas(conn, dbType) as any; + const liveSchemas = dbSchema.schemas; + + // Generate schema hash from live schemas + const crypto = require("crypto"); + const newHash = crypto.createHash("sha256").update(JSON.stringify(liveSchemas)).digest("hex"); + + // Fetch old schema file to compare + let oldHash = ""; + try { + const oldSchemaFile = await projectStoreInstance.getSchema(projectId); + oldHash = (oldSchemaFile as any)?.schemaHash || ""; + } catch (e) { + // Initial creation + } + + // Save anyway (or only if changed, but we can always save) + // projectStoreInstance.saveSchema now expects to write schemaHash and dialect. + // Let's call saveSchema and then update the schema.json directly or update saveSchema signature. + // Since saveSchema only takes `schemas` right now, I'll update saveSchema in projectStore shortly. + await projectStoreInstance.saveSchema(projectId, liveSchemas, newHash, dbType); + + if (newHash !== oldHash) { + this.rpc?.sendNotification?.("project.schema_changed", { projectId, newHash }); + + // Commit to Git if tracking + try { + const projectDir = await projectStoreInstance.resolveProjectDir(projectId); + if (projectDir) { + await gitServiceInstance.syncSchemaFile(projectDir); + } + } catch (gitErr) { + this.logger?.warn({ err: gitErr }, "Failed to auto-commit schema.json to Git"); + } + } + + this.rpc.sendResponse(id, { ok: true, hashChanged: newHash !== oldHash, newHash }); + } catch (e: any) { + this.logger?.error({ e }, "project.refreshSchemaCache failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + async handleGetERDiagram(params: any, id: number | string) { try { const { projectId } = params || {}; @@ -247,6 +314,263 @@ export class ProjectHandlers { } } + async handleAnalyzeImport(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + } + + const project = await projectStoreInstance.getProject(projectId); + if (!project) { + return this.rpc.sendError(id, { code: "NOT_FOUND", message: "Project not found" }); + } + + // 1. Read schema snapshot + const schemaFile = await projectStoreInstance.getSchema(projectId); + const hasSchemaSnapshot = !!schemaFile && !!(schemaFile as any)?.schemas?.length; + const schemaHash = (schemaFile as any)?.schemaHash || ""; + + // 2. Read migration files from project-local dir + const { listMigrationFiles, readMigrationFile } = await import("../utils/migrationFileReader"); + let migrationFiles: string[] = []; + let migrationsDir = ""; + if (project.databaseId) { + migrationsDir = await projectStoreInstance.resolveMigrationsDir(project.databaseId); + migrationFiles = listMigrationFiles(migrationsDir); + } + const hasMigrations = migrationFiles.length > 0; + + // 3. Read lock file + let lockFileStatus: "valid" | "tampered" | "missing" = "missing"; + let tamperedFiles: string[] = []; + let appliedVersions: string[] = []; + if (project.databaseId) { + const { readMigrationLock } = await import("../services/migrationLock"); + const lock = await readMigrationLock(project.databaseId); + if (lock) { + lockFileStatus = lock.schemaHash === schemaHash ? "valid" : "tampered"; + appliedVersions = lock.appliedMigrations || []; + } + } + + // 4. Query live database for table count + let targetDatabaseEmpty = true; + let targetTableCount = 0; + let dbAppliedVersions: string[] = []; + if (project.databaseId) { + try { + const { conn, dbType } = await this.dbService.getDatabaseConnection(project.databaseId); + let connector: any; + if (dbType === "mysql") connector = require("../connectors/mysql"); + else if (dbType === "postgres") connector = require("../connectors/postgres"); + else if (dbType === "mariadb") connector = require("../connectors/mariadb"); + else if (dbType === "sqlite") connector = require("../connectors/sqlite"); + + if (connector) { + try { + const tables = await connector.listTables(conn); + // Filter out internal migration tracking tables + const userTables = (tables || []).filter((t: any) => { + const name = typeof t === "string" ? t : t?.name || ""; + return name !== "__relwave_migrations" && name !== "relwave_migrations"; + }); + targetTableCount = userTables.length; + targetDatabaseEmpty = targetTableCount === 0; + } catch { + // Can't list tables — assume non-empty for safety + targetDatabaseEmpty = false; + } + + try { + const applied = await connector.listAppliedMigrations(conn); + dbAppliedVersions = (applied || []).map((m: any) => m.version); + } catch { + // Migration tracking table may not exist yet + } + } + } catch { + // Database connection may not be available + } + } + + // 5. Compute pending migrations + const appliedSet = new Set(dbAppliedVersions); + const pendingMigrations: Array<{ + file: string; + version: string; + isDestructive: boolean; + destructiveOps: string[]; + }> = []; + + for (const file of migrationFiles) { + const versionMatch = file.match(/^(\d{13,14})/); + if (!versionMatch) continue; + const version = versionMatch[1]; + if (appliedSet.has(version)) continue; + + // Parse for destructive operations + let isDestructive = false; + const destructiveOps: string[] = []; + try { + const parsed = readMigrationFile(path.join(migrationsDir, file)); + const upSQL = parsed.upSQL.toUpperCase(); + if (upSQL.includes("DROP TABLE")) { + isDestructive = true; + destructiveOps.push("DROP TABLE"); + } + if (upSQL.includes("DROP COLUMN")) { + isDestructive = true; + destructiveOps.push("DROP COLUMN"); + } + if (upSQL.includes("TRUNCATE")) { + isDestructive = true; + destructiveOps.push("TRUNCATE"); + } + } catch { + // Skip parse errors + } + + pendingMigrations.push({ file, version, isDestructive, destructiveOps }); + } + + // 6. Detect if all migration files are baseline-only (no real user migrations) + const isBaselineOnly = hasMigrations && migrationFiles.every(f => f.includes("baseline")); + + // 7. Compute drift status + let driftStatus: "synced" | "drifted" | "unknown" = "unknown"; + if (pendingMigrations.length === 0 && hasMigrations) { + // All migrations applied + driftStatus = "synced"; + } else if (pendingMigrations.length === 0 && !hasMigrations && !hasSchemaSnapshot) { + // No migrations or schema — fresh project + driftStatus = "synced"; + } else if (pendingMigrations.length > 0) { + driftStatus = "drifted"; + } else if (!hasMigrations && hasSchemaSnapshot && targetDatabaseEmpty) { + // Schema exists but database is empty — need to apply snapshot + driftStatus = "drifted"; + } else if (!hasMigrations && hasSchemaSnapshot && !targetDatabaseEmpty) { + // Schema exists and database has tables — assume synced (user just hasn't made migrations yet) + driftStatus = "synced"; + } + + // 8. Compute available modes + const availableModes: Array<"run_migrations" | "apply_snapshot" | "skip"> = ["skip"]; + let recommendedMode: "run_migrations" | "apply_snapshot" | "skip" = "skip"; + + if (isBaselineOnly && hasSchemaSnapshot && targetDatabaseEmpty) { + // Baseline-only + empty DB + schema.json exists: + // The baseline file has no real DDL — use schema.json to reconstruct the DB + availableModes.unshift("apply_snapshot"); + recommendedMode = "apply_snapshot"; + // Override: treat as "no real migrations" for the dialog + driftStatus = "drifted"; + } else if (hasMigrations && pendingMigrations.length > 0 && !isBaselineOnly) { + availableModes.unshift("run_migrations"); + recommendedMode = "run_migrations"; + } + if (hasSchemaSnapshot && targetDatabaseEmpty && !hasMigrations) { + availableModes.unshift("apply_snapshot"); + recommendedMode = "apply_snapshot"; + } + + // For the dialog: if baseline-only + empty DB, report as "no real migrations" + // so the frontend shows "Apply Schema Snapshot" (STATE 3) instead of + // "Pending Migrations" (STATE 2) which would try to run the empty baseline + const reportHasMigrations = isBaselineOnly && targetDatabaseEmpty ? false : hasMigrations; + const reportPendingMigrations = isBaselineOnly && targetDatabaseEmpty ? [] : pendingMigrations; + + const result = { + hasMigrations: reportHasMigrations, + migrationCount: reportPendingMigrations.length, + hasSchemaSnapshot, + lockFileStatus, + tamperedFiles, + targetDatabaseEmpty, + targetTableCount, + driftStatus, + driftDetails: undefined, + pendingMigrations: reportPendingMigrations, + availableModes, + recommendedMode, + }; + + this.rpc.sendResponse(id, { ok: true, data: result }); + } catch (e: any) { + this.logger?.error({ e }, "project.analyzeImport failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleVerifyLock(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + } + const project = await projectStoreInstance.getProject(projectId); + if (!project?.databaseId) { + return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Project has no database ID" }); + } + const { verifyMigrationLock } = await import("../services/migrationLock"); + const schemaFile = await projectStoreInstance.getSchema(projectId); + const schemaHash = (schemaFile as any)?.schemaHash || ""; + + const isValid = await verifyMigrationLock(project.databaseId, schemaHash); + this.rpc.sendResponse(id, { ok: true, isValid }); + } catch (e: any) { + this.logger?.error({ e }, "project.verifyLock failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handlePushMigrations(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + const projectDir = await projectStoreInstance.resolveProjectDir(projectId); + if (!projectDir) return this.rpc.sendError(id, { code: "NOT_FOUND", message: "Project directory not found" }); + + const { gitServiceInstance } = await import("../services/gitService"); + await gitServiceInstance.pushMigrations(projectDir); + this.rpc.sendResponse(id, { ok: true }); + } catch (e: any) { + this.logger?.error({ e }, "project.pushMigrations failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleSyncMigrations(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + const projectDir = await projectStoreInstance.resolveProjectDir(projectId); + if (!projectDir) return this.rpc.sendError(id, { code: "NOT_FOUND", message: "Project directory not found" }); + + const { gitServiceInstance } = await import("../services/gitService"); + const result = await gitServiceInstance.syncMigrationFiles(projectDir); + if (result.error) throw new Error(result.error.message || String(result.error)); + this.rpc.sendResponse(id, { ok: true, data: result }); + } catch (e: any) { + this.logger?.error({ e }, "project.syncMigrations failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleGetDrift(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) return this.rpc.sendError(id, { code: "BAD_REQUEST", message: "Missing projectId" }); + + // Stub implementation for now + this.rpc.sendResponse(id, { ok: true, data: { driftDetected: false, differences: [] } }); + } catch (e: any) { + this.logger?.error({ e }, "project.getDrift failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + async handleGetQueries(params: any, id: number | string) { try { const { projectId } = params || {}; @@ -430,7 +754,7 @@ export class ProjectHandlers { this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); } } - + /** Read-only scan — returns metadata + .env info without creating anything. */ async handleScanImport(params: any, id: number | string) { try { @@ -503,4 +827,115 @@ export class ProjectHandlers { this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); } } + + async handleUnlinkFromConnection(params: any, id: number | string) { + try { + const { databaseId } = params || {}; + if (!databaseId) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing databaseId", + }); + } + + await projectStoreInstance.unlinkDatabase(databaseId); + this.rpc.sendResponse(id, { ok: true }); + } catch (e: any) { + this.logger?.error({ e }, "project.unlinkFromConnection failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleDeleteWithConnection(params: any, id: number | string) { + try { + const { databaseId } = params || {}; + if (!databaseId) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing databaseId", + }); + } + + await projectStoreInstance.deleteProjectByDatabaseId(databaseId); + this.rpc.sendResponse(id, { ok: true }); + } catch (e: any) { + this.logger?.error({ e }, "project.deleteWithConnection failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + + async handleGetGitRemote(params: any, id: number | string) { + try { + const { projectPath } = params || {}; + if (!projectPath) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing projectPath", + }); + } + + // Using gitServiceInstance since it handles standard git operations + const remotes = await gitServiceInstance.remoteList(projectPath); + const remoteUrl = remotes.length > 0 ? remotes[0].fetchUrl : null; + + this.rpc.sendResponse(id, { ok: true, data: { remoteUrl } }); + } catch (e: any) { + // If it's not a git repo or fails, just return null + this.logger?.error({ e }, "project.getGitRemote failed (might not be a git repo)"); + this.rpc.sendResponse(id, { ok: true, data: { remoteUrl: null } }); + } + } + + async handleRelinkToConnection(params: any, id: number | string) { + try { + const { projectId, databaseId } = params || {}; + if (!projectId || !databaseId) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing projectId or databaseId", + }); + } + + const project = await projectStoreInstance.relinkDatabase(projectId, databaseId); + + this.rpc.sendResponse(id, { ok: true, data: project }); + } catch (e: any) { + this.logger?.error({ e }, "project.relinkToConnection failed"); + // Check if it's the specific error we throw + if (e.message?.includes("DATABASE_ALREADY_HAS_PROJECT")) { + this.rpc.sendError(id, { code: "DATABASE_ALREADY_HAS_PROJECT", message: e.message }); + } else { + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + } + + async handleGenerateSQL(params: any, id: number | string) { + try { + const { projectId } = params || {}; + if (!projectId) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing projectId", + }); + } + + const { projectStoreInstance } = await import("../services/projectStore"); + const schemaFile = await projectStoreInstance.getSchema(projectId); + if (!schemaFile) { + return this.rpc.sendError(id, { + code: "NOT_FOUND", + message: "No schema snapshot found", + }); + } + + const { generateBaselineSQL } = await import("../utils/baselineMigration"); + const sql = generateBaselineSQL(schemaFile as any, "preview", "preview"); + + this.rpc.sendResponse(id, { ok: true, data: { sql } }); + } catch (e: any) { + this.logger?.error({ e }, "project.generateSQL failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } } diff --git a/bridge/src/jsonRpcHandler.ts b/bridge/src/jsonRpcHandler.ts index 8b90f6c..e6604c5 100644 --- a/bridge/src/jsonRpcHandler.ts +++ b/bridge/src/jsonRpcHandler.ts @@ -65,10 +65,11 @@ export function registerDbHandlers( dbService, queryExecutor ); - const projectHandlers = new ProjectHandlers(rpc, logger); + const projectHandlers = new ProjectHandlers(rpc, logger, dbService, queryExecutor); const gitHandlers = new GitHandlers(rpc, logger); const gitAdvancedHandlers = new GitAdvancedHandlers(rpc, logger); const monitoringHandlers = new MonitoringHandlers(rpc, logger, dbService, monitoringService); + const aiHandlers = new (require("./handlers/aiHandlers")).AIHandlers(rpc, logger); // ========================================== // SESSION MANAGEMENT HANDLERS @@ -167,6 +168,12 @@ export function registerDbHandlers( rpcRegister(rpc, "migration.apply", (p, id) => migrationHandlers.handleApplyMigration(p, id) ); + rpcRegister(rpc, "migration.applyMigrations", (p, id) => + migrationHandlers.handleApplyMigrations(p, id) + ); + rpcRegister(rpc, "migration.applySnapshot", (p, id) => + migrationHandlers.handleApplySnapshot(p, id) + ); rpcRegister(rpc, "migration.rollback", (p, id) => migrationHandlers.handleRollbackMigration(p, id) ); @@ -226,6 +233,9 @@ export function registerDbHandlers( rpcRegister(rpc, "project.saveSchema", (p, id) => projectHandlers.handleSaveSchema(p, id) ); + rpcRegister(rpc, "project.refreshSchemaCache", (p, id) => + projectHandlers.handleRefreshSchemaCache(p, id) + ); rpcRegister(rpc, "project.getERDiagram", (p, id) => projectHandlers.handleGetERDiagram(p, id) ); @@ -238,6 +248,24 @@ export function registerDbHandlers( rpcRegister(rpc, "project.saveAnnotations", (p, id) => projectHandlers.handleSaveAnnotations(p, id) ); + rpcRegister(rpc, "project.analyzeImport", (p, id) => + projectHandlers.handleAnalyzeImport(p, id) + ); + rpcRegister(rpc, "project.verifyLock", (p, id) => + projectHandlers.handleVerifyLock(p, id) + ); + rpcRegister(rpc, "project.pushMigrations", (p, id) => + projectHandlers.handlePushMigrations(p, id) + ); + rpcRegister(rpc, "project.syncMigrations", (p, id) => + projectHandlers.handleSyncMigrations(p, id) + ); + rpcRegister(rpc, "project.generateSQL", (p, id) => + projectHandlers.handleGenerateSQL(p, id) + ); + rpcRegister(rpc, "project.getDrift", (p, id) => + projectHandlers.handleGetDrift(p, id) + ); rpcRegister(rpc, "project.getQueries", (p, id) => projectHandlers.handleGetQueries(p, id) ); @@ -274,6 +302,18 @@ export function registerDbHandlers( rpcRegister(rpc, "project.linkDatabase", (p, id) => projectHandlers.handleLinkDatabase(p, id) ); + rpcRegister(rpc, "project.unlinkFromConnection", (p, id) => + projectHandlers.handleUnlinkFromConnection(p, id) + ); + rpcRegister(rpc, "project.deleteWithConnection", (p, id) => + projectHandlers.handleDeleteWithConnection(p, id) + ); + rpcRegister(rpc, "project.getGitRemote", (p, id) => + projectHandlers.handleGetGitRemote(p, id) + ); + rpcRegister(rpc, "project.relinkToConnection", (p, id) => + projectHandlers.handleRelinkToConnection(p, id) + ); // ========================================== // GIT HANDLERS @@ -322,6 +362,34 @@ export function registerDbHandlers( } }); + // ========================================== + // AI HANDLERS + // ========================================== + rpcRegister(rpc, "ai.testConnection", (p, id) => + aiHandlers.handleTestConnection(p, id) + ); + rpcRegister(rpc, "ai.analyzeSchema", (p, id) => + aiHandlers.handleAnalyzeSchema(p, id) + ); + rpcRegister(rpc, "ai.explainQuery", (p, id) => + aiHandlers.handleExplainQuery(p, id) + ); + rpcRegister(rpc, "ai.recommendChart", (p, id) => + aiHandlers.handleRecommendChart(p, id) + ); + rpcRegister(rpc, "ai.getHistory", (p, id) => + aiHandlers.handleGetHistory(p, id) + ); + rpcRegister(rpc, "ai.getHistoryById", (p, id) => + aiHandlers.handleGetHistoryById(p, id) + ); + rpcRegister(rpc, "ai.deleteHistory", (p, id) => + aiHandlers.handleDeleteHistory(p, id) + ); + rpcRegister(rpc, "ai.clearHistory", (p, id) => + aiHandlers.handleClearHistory(p, id) + ); + logger?.info("All RPC handlers registered successfully"); } diff --git a/bridge/src/queries/README.md b/bridge/src/queries/README.md new file mode 100644 index 0000000..c8ad53f --- /dev/null +++ b/bridge/src/queries/README.md @@ -0,0 +1,26 @@ +# SQL Queries (Bridge) + +This folder contains centralized SQL query strings and SQL helper functions for supported database engines. + +Layout +- `index.ts` - central export for all query modules. +- `postgres/` - PostgreSQL SQL grouped by schema, tables, constraints, stats, migrations and CRUD. +- `mysql/` - MySQL SQL grouped by schema, tables, columns, constraints, stats, migrations and CRUD. +- `sqlite/` - SQLite SQL grouped by schema, tables, constraints, stats, migrations and CRUD. + +How it fits +- Connectors import query constants from this folder and bind parameters through their database driver. +- Query modules keep SQL text out of service and handler code. +- Database-specific quoting helpers, such as CRUD identifier quoting, live beside the SQL for that engine. + +How to add a query +1. Add the SQL constant or helper to the matching database folder. +2. Export it from that database folder's `index.ts` if other modules need central imports. +3. Use parameter placeholders supported by the target driver. +4. Keep result column aliases compatible with `src/types/common.ts` where possible. +5. Add database-specific types under `src/types` only when the common shape is not enough. + +Notes +- Do not concatenate user-controlled identifiers or values directly into SQL. Use driver parameters for values and vetted quote helpers for identifiers. +- Keep equivalent queries across PostgreSQL, MySQL/MariaDB and SQLite aligned so `QueryExecutor` can return consistent frontend shapes. +- If a query intentionally excludes system schemas or internal tables, document that behavior near the SQL constant. diff --git a/bridge/src/queries/mysql/columns.ts b/bridge/src/queries/mysql/columns.ts index 903d16c..d112632 100644 --- a/bridge/src/queries/mysql/columns.ts +++ b/bridge/src/queries/mysql/columns.ts @@ -16,7 +16,11 @@ export const BATCH_GET_ALL_COLUMNS = ` c.ORDINAL_POSITION AS ordinal_position, c.CHARACTER_MAXIMUM_LENGTH AS max_length, (c.COLUMN_KEY = 'PRI') AS is_primary_key, - CASE WHEN fk.COLUMN_NAME IS NOT NULL THEN TRUE ELSE FALSE END AS is_foreign_key + CASE WHEN fk.COLUMN_NAME IS NOT NULL THEN TRUE ELSE FALSE END AS is_foreign_key, + (c.COLUMN_KEY = 'UNI' OR c.COLUMN_KEY = 'PRI') AS is_unique, + (c.EXTRA LIKE '%auto_increment%') AS is_serial, + c.COLUMN_COMMENT AS comment, + NULL AS check_constraint FROM information_schema.columns c LEFT JOIN ( SELECT DISTINCT kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME diff --git a/bridge/src/queries/postgres/tables.ts b/bridge/src/queries/postgres/tables.ts index a3fa7fa..423019a 100644 --- a/bridge/src/queries/postgres/tables.ts +++ b/bridge/src/queries/postgres/tables.ts @@ -28,8 +28,7 @@ export const PG_GET_TABLE_DETAILS = ` `; /** - * Get all columns in a schema (batch query) - * @param schemaName - Use $1 placeholder + * Batch get all columns for all tables in a schema */ export const PG_BATCH_GET_ALL_COLUMNS = ` SELECT @@ -56,7 +55,27 @@ export const PG_BATCH_GET_ALL_COLUMNS = ` AND tc.table_name = c.table_name AND kcu.column_name = c.column_name), FALSE - ) AS is_foreign_key + ) AS is_foreign_key, + COALESCE( + (SELECT TRUE FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name + WHERE tc.constraint_type = 'UNIQUE' + AND tc.table_schema = c.table_schema + AND tc.table_name = c.table_name + AND kcu.column_name = c.column_name), + FALSE + ) AS is_unique, + (c.column_default ILIKE '%nextval%' OR c.is_identity = 'YES') AS is_serial, + (SELECT pg_catalog.col_description(format('%I.%I', c.table_schema, c.table_name)::regclass::oid, c.ordinal_position)) AS comment, + (SELECT cc.check_clause + FROM information_schema.table_constraints tc + JOIN information_schema.check_constraints cc ON tc.constraint_name = cc.constraint_name + JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name + WHERE tc.constraint_type = 'CHECK' + AND tc.table_schema = c.table_schema + AND tc.table_name = c.table_name + AND ccu.column_name = c.column_name + LIMIT 1) AS check_constraint FROM information_schema.columns c WHERE c.table_schema = $1 ORDER BY c.table_name, c.ordinal_position; diff --git a/bridge/src/services/README.md b/bridge/src/services/README.md new file mode 100644 index 0000000..b248036 --- /dev/null +++ b/bridge/src/services/README.md @@ -0,0 +1,37 @@ +# Services (Bridge) + +This folder contains bridge business logic and persistence services. Services sit between RPC handlers and low-level connectors, stores, keyrings, Git, filesystem and monitoring utilities. + +Layout +- `databaseService.ts` - database metadata lifecycle, credential lookup and connection config retrieval. +- `queryExecutor.ts` - database-agnostic dispatcher for schema, table, query, CRUD and migration operations. +- `connectionBuilder.ts` - builds driver config objects and optional SSH tunnel wiring. +- `connectionPool.ts` - caches resolved connection configs and tunnel handles by database id. +- `connectorRegistry.ts` - maps database types to connector implementations. +- `dbStore.ts` - persisted database connection metadata and credential references. +- `projectStore.ts` - project files, schema snapshots, ER diagrams, annotations, saved queries and local project config. +- `gitService.ts` - local Git operations used by Git handlers. +- `keyringService.ts` - encrypted credential storage through `@napi-rs/keyring`. +- `sshTunnelService.ts` - SSH tunnel creation and cleanup. +- `discoveryService.ts` - local database discovery. +- `monitoringService.ts` and `monitoringWebSocketServer.ts` - database monitoring snapshots and websocket broadcast support. +- `ai.impl.ts`, `aiService.ts`, `aiCacheService.ts`, `aiHistoryStore.ts` - AI service factory, compatibility shim, cache and history persistence. +- `logger.ts` - shared pino logger. + +How it fits +- Handlers call services to do real work; services should not know about UI components. +- Services call connectors, stores and utilities, then return plain data to handlers. +- Persistent paths are defined in `src/utils/config.ts`. +- Shared types come from `src/types`. + +How to add service behavior +1. Put durable business logic here when it is shared by multiple handlers or has state/persistence concerns. +2. Keep public methods small and typed enough for handler use. +3. Let handlers translate thrown errors into JSON-RPC errors. +4. Invalidate connection or metadata caches when mutating database/project state. +5. Add focused tests under `bridge/__tests__` for services with persistence, Git, credential or connector logic. + +Notes +- Keep secrets out of logs and return values. +- Avoid long-lived open database sockets unless the connection pool explicitly owns them. +- When adding filesystem writes, use paths derived from `src/utils/config.ts` so RelWave respects `RELWAVE_HOME`. diff --git a/bridge/src/services/ai.impl.ts b/bridge/src/services/ai.impl.ts new file mode 100644 index 0000000..3554216 --- /dev/null +++ b/bridge/src/services/ai.impl.ts @@ -0,0 +1,60 @@ +import { AISettings, AIProviderName } from "../types/ai"; +import { AIProvider, AIError } from "../ai/providers/types"; +import { AnthropicProvider } from "../ai/providers/anthropic.provider"; +import { OpenAIProvider } from "../ai/providers/openai.provider"; +import { GeminiProvider } from "../ai/providers/gemini.provider"; +import { GroqProvider } from "../ai/providers/groq.provider"; +import { MistralProvider } from "../ai/providers/mistral.provider"; +import { OllamaProvider } from "../ai/providers/ollama.provider"; + +/** + * Factory — creates the correct provider from user settings. + * Throws AIError("MISSING_API_KEY") if the required credential is absent. + */ +export class AIServiceImpl { + /** + * Resolve the active provider from the supplied settings. + */ + resolveProvider(settings: AISettings): AIProvider { + const provider = settings.defaultProvider; + return this.createProvider(provider, settings); + } + + private createProvider(name: AIProviderName, settings: AISettings): AIProvider { + switch (name) { + case "anthropic": { + const key = settings.anthropicApiKey?.trim(); + if (!key) throw new AIError("MISSING_API_KEY", "anthropic", "Anthropic API key is not configured."); + return new AnthropicProvider(key); + } + case "openai": { + const key = settings.openaiApiKey?.trim(); + if (!key) throw new AIError("MISSING_API_KEY", "openai", "OpenAI API key is not configured."); + return new OpenAIProvider(key); + } + case "gemini": { + const key = settings.geminiApiKey?.trim(); + if (!key) throw new AIError("MISSING_API_KEY", "gemini", "Gemini API key is not configured."); + return new GeminiProvider(key); + } + case "groq": { + const key = settings.groqApiKey?.trim(); + if (!key) throw new AIError("MISSING_API_KEY", "groq", "Groq API key is not configured."); + return new GroqProvider(key); + } + case "mistral": { + const key = settings.mistralApiKey?.trim(); + if (!key) throw new AIError("MISSING_API_KEY", "mistral", "Mistral API key is not configured."); + return new MistralProvider(key); + } + case "ollama": { + return new OllamaProvider(settings.ollamaBaseUrl, settings.ollamaModel); + } + default: { + throw new AIError("PROVIDER_UNAVAILABLE", name as string, `Unknown provider: ${name}`); + } + } + } +} + +export const aiImpl = new AIServiceImpl(); diff --git a/bridge/src/services/aiCacheService.ts b/bridge/src/services/aiCacheService.ts new file mode 100644 index 0000000..6294668 --- /dev/null +++ b/bridge/src/services/aiCacheService.ts @@ -0,0 +1,172 @@ +/** + * AI Cache Service — orchestrates cache-first AI calls. + * + * Before calling an LLM provider, generates a deterministic SHA-256 hash + * of the input and checks the local SQLite history for a cached response. + * If found, returns immediately without an API call. Otherwise, calls the + * provider, stores the result, and returns it. + */ + +import crypto from "crypto"; +import { aiHistoryStore, type AIHistoryInsert } from "./aiHistoryStore"; +import { AIProvider } from "../ai/providers/types"; +import { + AISettings, + SchemaAnalysisInput, + QueryExplanationInput, + ChartRecommendationInput, +} from "../types/ai"; + +// ── Types ───────────────────────────────────────────────────────────────── + +export type AIFeature = "schema-analysis" | "query-explanation" | "chart-recommendation"; + +export interface CachedResult { + response: string; + cached: boolean; + createdAt?: string; +} + +// ── Hashing ─────────────────────────────────────────────────────────────── + +/** + * Generate a deterministic SHA-256 hash for the given feature and input. + * The same input always produces the same hash. + */ +export function generateContentHash(feature: AIFeature, data: unknown): string { + const payload = JSON.stringify({ feature, data }, Object.keys({ feature, data }).sort()); + return crypto.createHash("sha256").update(payload, "utf-8").digest("hex"); +} + +/** Hash for schema analysis: uses datasource + tables, columns, indexes, relationships, constraints. */ +export function hashSchemaAnalysis(input: SchemaAnalysisInput, datasourceName?: string): string { + const normalized = { + datasource: datasourceName ?? null, + databaseType: input.databaseType ?? null, + tables: (input.tables ?? []).map((t) => ({ + name: t.name, + schema: t.schema ?? null, + columns: (t.columns ?? []).map((c) => ({ + name: c.name, + type: c.type, + nullable: c.nullable ?? null, + isPrimaryKey: c.isPrimaryKey ?? null, + isForeignKey: c.isForeignKey ?? null, + references: c.references ?? null, + })), + indexes: t.indexes ?? [], + foreignKeys: t.foreignKeys ?? [], + constraints: t.constraints ?? [], + })), + }; + return generateContentHash("schema-analysis", normalized); +} + +/** Hash for query explanation: uses datasource + sql + schema. */ +export function hashQueryExplanation(input: QueryExplanationInput, datasourceName?: string): string { + const normalized = { + datasource: datasourceName ?? null, + sql: (input.sql ?? "").trim(), + databaseType: input.databaseType ?? null, + schema: (input.schema ?? []).map((t) => ({ + name: t.name, + schema: t.schema ?? null, + columns: (t.columns ?? []).map((c) => ({ + name: c.name, + type: c.type, + })), + })), + }; + return generateContentHash("query-explanation", normalized); +} + +/** Hash for chart recommendation: uses datasource + tableName + columns. */ +export function hashChartRecommendation(input: ChartRecommendationInput, datasourceName?: string): string { + const normalized = { + datasource: datasourceName ?? null, + tableName: input.tableName, + columns: (input.columns ?? []).map((c) => ({ + name: c.name, + type: c.type, + isPrimaryKey: c.isPrimaryKey ?? null, + })), + }; + return generateContentHash("chart-recommendation", normalized); +} + +// ── Cache-first orchestration ───────────────────────────────────────────── + +/** + * Resolve the model name from the settings based on provider. + * This is best-effort — some providers don't expose the model in settings. + */ +function resolveModelName(settings: AISettings): string { + const provider = settings.defaultProvider; + switch (provider) { + case "ollama": + return settings.ollamaModel ?? "ollama-default"; + default: + return provider; // For API-key providers, the model is selected by the SDK + } +} + +/** + * Check cache, and if missed, call the provider and store the result. + * + * @param feature The AI feature name (schema-analysis, query-explanation, chart-recommendation) + * @param hash Content hash for cache lookup + * @param settings User's AI settings (provider, keys) + * @param prompt The prompt text (for logging / history display) + * @param callFn Async function that calls the actual AI provider + * @param meta Optional metadata (datasource_id, table_name) + * @param skipCache If true, bypass cache and force a fresh call + */ +export async function getOrCall(opts: { + feature: AIFeature; + hash: string; + settings: AISettings; + prompt: string; + callFn: () => Promise; + meta?: { datasource_id?: string; table_name?: string }; + skipCache?: boolean; +}): Promise { + const { feature, hash, settings, prompt, callFn, meta, skipCache } = opts; + + // Check cache (unless skipCache is true) + if (!skipCache) { + const cached = aiHistoryStore.findByHash(hash, meta?.datasource_id); + if (cached) { + return { + response: cached.response, + cached: true, + createdAt: cached.created_at, + }; + } + } + + // Cache miss — call the provider + const response = await callFn(); + + // Estimate tokens: ~4 characters per token is a reasonable approximation + // for English text across most LLM tokenizers. + const estimatedTokens = Math.ceil((prompt.length + response.length) / 4); + + // Store in history + const record: AIHistoryInsert = { + feature, + datasource_id: meta?.datasource_id ?? null, + table_name: meta?.table_name ?? null, + content_hash: hash, + provider: settings.defaultProvider, + model: resolveModelName(settings), + prompt, + response, + tokens_used: estimatedTokens, + }; + aiHistoryStore.insert(record); + + return { + response, + cached: false, + }; +} diff --git a/bridge/src/services/aiHistoryStore.ts b/bridge/src/services/aiHistoryStore.ts new file mode 100644 index 0000000..726f8c1 --- /dev/null +++ b/bridge/src/services/aiHistoryStore.ts @@ -0,0 +1,258 @@ +/** + * AI History Store — SQLite-backed repository for AI analysis history. + * + * Stores AI responses locally so the user can browse previous analyses, + * and provides the persistence layer for the cache system. + */ + +import Database from "better-sqlite3"; +import path from "path"; +import { resolvePkgNativeBindingPath } from "../connectors/sqlite"; +import fs from "fs"; +import { CONFIG_FOLDER } from "../utils/config"; + +// ── Types ───────────────────────────────────────────────────────────────── + +export interface AIHistoryRow { + id: number; + feature: string; + datasource_id: string | null; + table_name: string | null; + content_hash: string | null; + provider: string; + model: string; + prompt: string; + response: string; + tokens_used: number | null; + created_at: string; +} + +/** Subset returned when listing (no prompt/response to keep payloads small). */ +export interface AIHistoryListItem { + id: number; + feature: string; + datasource_id: string | null; + table_name: string | null; + provider: string; + model: string; + tokens_used: number | null; + created_at: string; +} + +export interface AIHistoryInsert { + feature: string; + datasource_id?: string | null; + table_name?: string | null; + content_hash?: string | null; + provider: string; + model: string; + prompt: string; + response: string; + tokens_used?: number | null; +} + +export interface AIHistoryListParams { + feature?: string; + provider?: string; + limit?: number; + offset?: number; +} + +export interface AIHistoryListResult { + items: AIHistoryListItem[]; + total: number; +} + +// ── DDL ─────────────────────────────────────────────────────────────────── + +const CREATE_TABLE_SQL = ` +CREATE TABLE IF NOT EXISTS ai_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + feature TEXT NOT NULL, + datasource_id TEXT, + table_name TEXT, + content_hash TEXT, + provider TEXT NOT NULL, + model TEXT NOT NULL, + prompt TEXT NOT NULL, + response TEXT NOT NULL, + tokens_used INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +`; + +const CREATE_INDEX_HASH = `CREATE INDEX IF NOT EXISTS idx_ai_history_hash ON ai_history(content_hash);`; +const CREATE_INDEX_HASH_DS = `CREATE INDEX IF NOT EXISTS idx_ai_history_hash_ds ON ai_history(content_hash, datasource_id);`; +const CREATE_INDEX_FEATURE = `CREATE INDEX IF NOT EXISTS idx_ai_history_feature ON ai_history(feature);`; +const CREATE_INDEX_CREATED = `CREATE INDEX IF NOT EXISTS idx_ai_history_created_at ON ai_history(created_at);`; + +// ── Store ───────────────────────────────────────────────────────────────── + +export class AIHistoryStore { + private db: Database.Database | null = null; + private dbPath: string; + + constructor(dbPath?: string) { + this.dbPath = dbPath ?? path.join(CONFIG_FOLDER, "ai_history.db"); + } + + /** Lazily open the database and run migrations. */ + private getDb(): Database.Database { + if (this.db) return this.db; + + // Ensure the config directory exists + const dir = path.dirname(this.dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Resolve native binding for pkg builds (reuse existing pattern) + let nativeBinding: string | undefined; + try { + nativeBinding = resolvePkgNativeBindingPath() ?? undefined; + } catch { + // Ignore if it fails for any reason + } + + const opts: Database.Options = {}; + if (nativeBinding) opts.nativeBinding = nativeBinding; + + this.db = new Database(this.dbPath, opts); + + // Enable WAL mode for better concurrent read performance + this.db.pragma("journal_mode = WAL"); + + // Run migrations + this.db.exec(CREATE_TABLE_SQL); + this.db.exec(CREATE_INDEX_HASH); + this.db.exec(CREATE_INDEX_HASH_DS); + this.db.exec(CREATE_INDEX_FEATURE); + this.db.exec(CREATE_INDEX_CREATED); + + return this.db; + } + + /** Insert a new history record. Returns the new row ID. */ + insert(record: AIHistoryInsert): number { + const db = this.getDb(); + const stmt = db.prepare(` + INSERT INTO ai_history (feature, datasource_id, table_name, content_hash, provider, model, prompt, response, tokens_used, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + const info = stmt.run( + record.feature, + record.datasource_id ?? null, + record.table_name ?? null, + record.content_hash ?? null, + record.provider, + record.model, + record.prompt, + record.response, + record.tokens_used ?? null, + new Date().toISOString() + ); + return info.lastInsertRowid as number; + } + + /** + * Look up a cached result by content_hash AND datasource_id. + * Both must match — a cached response for Database A won't be + * returned when querying Database B, even with the same schema. + */ + findByHash(contentHash: string, datasourceId?: string | null): AIHistoryRow | null { + const db = this.getDb(); + let row: AIHistoryRow | undefined; + + if (datasourceId) { + // Match both hash and the specific datasource + row = db + .prepare( + `SELECT * FROM ai_history WHERE content_hash = ? AND datasource_id = ? ORDER BY created_at DESC LIMIT 1` + ) + .get(contentHash, datasourceId) as AIHistoryRow | undefined; + } else { + // No datasource specified — only match rows that also have no datasource + row = db + .prepare( + `SELECT * FROM ai_history WHERE content_hash = ? AND datasource_id IS NULL ORDER BY created_at DESC LIMIT 1` + ) + .get(contentHash) as AIHistoryRow | undefined; + } + + return row ?? null; + } + + /** List history entries with optional filters and pagination. */ + list(params: AIHistoryListParams = {}): AIHistoryListResult { + const db = this.getDb(); + const conditions: string[] = []; + const values: unknown[] = []; + + if (params.feature) { + conditions.push("feature = ?"); + values.push(params.feature); + } + if (params.provider) { + conditions.push("provider = ?"); + values.push(params.provider); + } + + const where = conditions.length + ? `WHERE ${conditions.join(" AND ")}` + : ""; + + const limit = params.limit ?? 20; + const offset = params.offset ?? 0; + + // Count + const countRow = db + .prepare(`SELECT COUNT(*) AS total FROM ai_history ${where}`) + .get(...values) as { total: number }; + + // Fetch items (no prompt/response for list view) + const items = db + .prepare( + `SELECT id, feature, datasource_id, table_name, provider, model, tokens_used, created_at + FROM ai_history ${where} + ORDER BY created_at DESC + LIMIT ? OFFSET ?` + ) + .all(...values, limit, offset) as AIHistoryListItem[]; + + return { items, total: countRow.total }; + } + + /** Get a single history entry by ID (full record). */ + getById(id: number): AIHistoryRow | null { + const db = this.getDb(); + const row = db + .prepare(`SELECT * FROM ai_history WHERE id = ?`) + .get(id) as AIHistoryRow | undefined; + return row ?? null; + } + + /** Delete a single history entry. Returns true if deleted. */ + deleteById(id: number): boolean { + const db = this.getDb(); + const info = db.prepare(`DELETE FROM ai_history WHERE id = ?`).run(id); + return info.changes > 0; + } + + /** Clear all history entries. Returns number of rows deleted. */ + clearAll(): number { + const db = this.getDb(); + const info = db.prepare(`DELETE FROM ai_history`).run(); + return info.changes; + } + + /** Close the database connection (for clean shutdown). */ + close(): void { + if (this.db) { + this.db.close(); + this.db = null; + } + } +} + +// Singleton instance +export const aiHistoryStore = new AIHistoryStore(); diff --git a/bridge/src/services/aiService.ts b/bridge/src/services/aiService.ts new file mode 100644 index 0000000..09036ac --- /dev/null +++ b/bridge/src/services/aiService.ts @@ -0,0 +1,7 @@ +import { AIServiceImpl } from "./ai.impl"; + +// Re-export the AIService class for discoverability under services/ +export class AIService extends AIServiceImpl {} + +// Also provide a singleton instance for callers that prefer a shared object +export const aiService = new AIService(); diff --git a/bridge/src/services/gitService.ts b/bridge/src/services/gitService.ts index f96c04e..fbf5b5f 100644 --- a/bridge/src/services/gitService.ts +++ b/bridge/src/services/gitService.ts @@ -87,6 +87,23 @@ export interface GitBranchInfo { upstream: string | null; } +export type GitSyncResult = { + staged: string[]; + commitHash: string; + commitMessage: string; + pushed: boolean; + error?: { code: string; message: string }; +}; + +export class GitError extends Error { + code: string; + constructor(code: string, message: string) { + super(message); + this.code = code; + this.name = "GitError"; + } +} + export class GitService { /** * Run a git command in a specific directory. @@ -1088,6 +1105,205 @@ export class GitService { async renameBranch(dir: string, newName: string): Promise { await this.git(dir, "branch", "-m", newName); } + + // ========================================== + // Migration Sync (Task 1) + // ========================================== + + /** + * Stage migration files and lock file + */ + async stageMigrationFiles(projectPath: string): Promise { + // Run git add on migrations/ and migration-lock.json + // But first check if they exist or have changes + const filesToStage = ["migrations"]; + const stagedPaths: string[] = []; + + for (const file of filesToStage) { + try { + await this.git(projectPath, "add", file); + stagedPaths.push(file); + } catch (err) { + // Ignore if path doesn't exist + } + } + + // Return actually staged files from status + const changes = await this.getChangedFiles(projectPath); + return changes.filter(c => c.staged).map(c => c.path); + } + + /** + * Commit migration files with an auto-generated or provided message + */ + async commitMigrationFiles(projectPath: string, message?: string): Promise { + let commitMsg = message; + + if (!commitMsg) { + // Auto-generate message based on staged files + const changes = await this.getChangedFiles(projectPath); + const stagedMigrations = changes.filter(c => c.staged && c.path.startsWith("migrations/")); + + if (stagedMigrations.length === 1) { + const filename = path.basename(stagedMigrations[0].path); + if (filename.includes("baseline")) { + commitMsg = "migrate: add baseline migration from schema snapshot"; + } else { + commitMsg = `migrate: apply ${filename}`; + } + } else if (stagedMigrations.length > 1) { + // Find highest migration + const sorted = stagedMigrations.map(m => path.basename(m.path)).sort(); + const latest = sorted[sorted.length - 1]; + commitMsg = `migrate: apply ${stagedMigrations.length} migrations up to ${latest}`; + } else { + commitMsg = "migrate: update schema lock"; + } + } + + return this.commit(projectPath, commitMsg); + } + + /** + * Push current branch to remote + */ + async pushMigrations(projectPath: string): Promise { + const remotes = await this.remoteList(projectPath); + if (remotes.length === 0) { + throw new GitError("NO_REMOTE", "No remote configured for this repository"); + } + + try { + await this.git(projectPath, "push"); + } catch (err: any) { + const errorMsg = err.stderr || err.message || ""; + if (errorMsg.includes("non-fast-forward") || errorMsg.includes("fetch first")) { + throw new GitError("PUSH_REJECTED", "Push rejected due to diverged history"); + } + throw new GitError("PUSH_FAILED", `Push failed: ${errorMsg}`); + } + } + + /** + * Orchestrate staging, committing, and pushing + */ + async syncMigrationFiles(projectPath: string, message?: string): Promise { + const result: GitSyncResult = { + staged: [], + commitHash: "", + commitMessage: "", + pushed: false, + }; + + try { + result.staged = await this.stageMigrationFiles(projectPath); + if (result.staged.length === 0) { + return result; // Nothing to sync + } + + result.commitHash = await this.commitMigrationFiles(projectPath, message); + + // Get actual message used if auto-generated + if (!message && result.commitHash) { + const log = await this.log(projectPath, 1); + if (log.length > 0) result.commitMessage = log[0].subject; + } else { + result.commitMessage = message || ""; + } + + // Read config to see if autoPushMigrations is true + let autoPush = false; + try { + const configPath = path.join(projectPath, "relwave.json"); + if (fsSync.existsSync(configPath)) { + const config = JSON.parse(fsSync.readFileSync(configPath, "utf-8")); + if (config.autoPushMigrations === true) { + autoPush = true; + } + } + } catch (err) { + // Ignore config read errors + } + + if (autoPush) { + try { + await this.pushMigrations(projectPath); + result.pushed = true; + } catch (pushErr: any) { + result.error = { + code: pushErr.code || "PUSH_FAILED", + message: pushErr.message + }; + } + } + + } catch (err: any) { + result.error = { + code: err.code || "SYNC_FAILED", + message: err.message + }; + } + + return result; + } + + /** + * Stash, sync schema.json + */ + async syncSchemaFile(projectPath: string, message?: string): Promise { + try { + await this.ensureGitignore(projectPath); + const changes = await this.getChangedFiles(projectPath); + const schemaFile = "schema/schema.json"; + + const schemaModified = changes.some(c => c.path === schemaFile); + + if (!schemaModified) { + return { staged: [], commitHash: "", commitMessage: "", pushed: false }; + } + + await this.stageFiles(projectPath, [schemaFile]); + + const commitMessage = message || `chore: update schema.json cache`; + const commitHash = await this.commit(projectPath, commitMessage); + + let pushed = false; + try { + await this.pushMigrations(projectPath); + pushed = true; + } catch (pushErr: any) { + // Return result with push error but still considered successful commit + return { + staged: [schemaFile], + commitHash, + commitMessage, + pushed: false, + error: { + code: "GIT_PUSH_FAILED", + message: `Committed successfully but failed to push: ${pushErr.message || String(pushErr)}` + } + }; + } + + return { + staged: [schemaFile], + commitHash, + commitMessage, + pushed + }; + } catch (error: any) { + return { + staged: [], + commitHash: "", + commitMessage: "", + pushed: false, + error: { + code: "GIT_SYNC_FAILED", + message: `Failed to sync schema file: ${error.message || String(error)}` + } + }; + } + } } export const gitServiceInstance = new GitService(); diff --git a/bridge/src/services/migrationLock.ts b/bridge/src/services/migrationLock.ts new file mode 100644 index 0000000..efa358b --- /dev/null +++ b/bridge/src/services/migrationLock.ts @@ -0,0 +1,51 @@ +import fs from "fs/promises"; +import path from "path"; +import fsSync from "fs"; +import { projectStoreInstance } from "./projectStore"; + +export interface MigrationLock { + version: string; + schemaHash: string; + appliedMigrations: string[]; // List of migration filenames + updatedAt: string; +} + +export async function getLockFilePath(dbId: string): Promise { + const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId); + return path.join(migrationsDir, "migration.lock.json"); +} + +export async function readMigrationLock(dbId: string): Promise { + const lockPath = await getLockFilePath(dbId); + if (!fsSync.existsSync(lockPath)) return null; + + try { + const raw = await fs.readFile(lockPath, "utf8"); + return JSON.parse(raw) as MigrationLock; + } catch (err) { + return null; + } +} + +export async function writeMigrationLock( + dbId: string, + schemaHash: string, + appliedMigrations: string[] +): Promise { + const lockPath = await getLockFilePath(dbId); + const lockData: MigrationLock = { + version: "1", + schemaHash, + appliedMigrations, + updatedAt: new Date().toISOString() + }; + + await fs.mkdir(path.dirname(lockPath), { recursive: true }).catch(() => {}); + await fs.writeFile(lockPath, JSON.stringify(lockData, null, 2), "utf8"); +} + +export async function verifyMigrationLock(dbId: string, targetHash: string): Promise { + const lock = await readMigrationLock(dbId); + if (!lock) return false; + return lock.schemaHash === targetHash; +} diff --git a/bridge/src/services/projectStore.ts b/bridge/src/services/projectStore.ts index 79c93c2..01af84a 100644 --- a/bridge/src/services/projectStore.ts +++ b/bridge/src/services/projectStore.ts @@ -21,7 +21,8 @@ import { dbStoreInstance, DBMeta } from "./dbStore"; export type ProjectMetadata = { version: number; id: string; - databaseId: string; + databaseId: string | null; + status?: "active" | "unlinked"; name: string; description?: string; engine?: string; @@ -74,22 +75,31 @@ export type AnnotationsFile = { }; export type SchemaFile = { - version: number; + version: number; // bumped to 2 projectId: string; databaseId: string; + dialect: "postgresql" | "mysql" | "sqlite" | "unknown"; schemas: SchemaSnapshot[]; cachedAt: string; + relwaveVersion: string; + schemaHash: string; // SHA-256 of entire schema content }; export type SchemaSnapshot = { name: string; tables: TableSnapshot[]; + enums?: EnumSnapshot[]; + views?: ViewSnapshot[]; }; export type TableSnapshot = { name: string; type: string; columns: ColumnSnapshot[]; + indexes: IndexSnapshot[]; + foreignKeys: ForeignKeySnapshot[]; + checks: CheckConstraint[]; + comment?: string | null; }; export type ColumnSnapshot = { @@ -100,11 +110,54 @@ export type ColumnSnapshot = { isForeignKey: boolean; defaultValue: string | null; isUnique: boolean; + isSerial: boolean; + ordinalPosition: number; + foreignKey?: { + schema: string; + table: string; + column: string; + onDelete?: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION"; + onUpdate?: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION"; + }; + checkConstraint?: string; + comment?: string | null; +}; + +export type IndexSnapshot = { + name: string; + columns: string[]; + unique: boolean; + partial?: string | null; +}; + +export type ForeignKeySnapshot = { + name: string; + columns: string[]; + referencedSchema: string; + referencedTable: string; + referencedColumns: string[]; + onDelete: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION" | "SET DEFAULT"; + onUpdate: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION" | "SET DEFAULT"; +}; + +export type CheckConstraint = { + name: string; + expression: string; +}; + +export type EnumSnapshot = { + name: string; + values: string[]; +}; + +export type ViewSnapshot = { + name: string; + definition: string; }; export type ProjectSummary = Pick< ProjectMetadata, - "id" | "name" | "description" | "engine" | "databaseId" | "sourcePath" | "createdAt" | "updatedAt" + "id" | "name" | "description" | "engine" | "databaseId" | "sourcePath" | "createdAt" | "updatedAt" | "status" >; /** @@ -339,7 +392,7 @@ export class ProjectStore { // Resolve engine from the linked database let engine: string | undefined; try { - const db: DBMeta | null = await dbStoreInstance.getDB(params.databaseId); + const db: DBMeta | undefined = await dbStoreInstance.getDB(params.databaseId); engine = db?.type; } catch { // db may not exist yet — that's OK @@ -356,6 +409,7 @@ export class ProjectStore { description: params.description, engine, defaultSchema: params.defaultSchema, + status: "active", createdAt: now, updatedAt: now, }; @@ -371,11 +425,14 @@ export class ProjectStore { // Initialise empty sub-files const emptySchema: SchemaFile = { - version: 1, + version: 2, projectId: id, databaseId: params.databaseId, + dialect: engine === "postgresql" ? "postgresql" : engine === "mysql" ? "mysql" : "sqlite", // Provide fallback or infer schemas: [], cachedAt: now, + relwaveVersion: "1.0.0", // Hardcoded for now + schemaHash: "", }; const emptyER: ERDiagramFile = { version: 1, @@ -416,6 +473,7 @@ export class ProjectStore { description: meta.description, engine, databaseId: meta.databaseId, + status: "active", createdAt: now, updatedAt: now, }); @@ -509,7 +567,7 @@ export class ProjectStore { // Resolve engine from the linked database let engine: string | undefined; try { - const db: DBMeta | null = await dbStoreInstance.getDB(databaseId); + const db: DBMeta | undefined = await dbStoreInstance.getDB(databaseId); engine = db?.type; } catch { // db may not exist yet @@ -566,12 +624,161 @@ export class ProjectStore { }; } + /** + * Unlink a database connection from a project (leaving the project orphaned but intact) + */ + async unlinkDatabase(databaseId: string): Promise { + const meta = await this.getProjectByDatabaseId(databaseId); + if (!meta) return; + + const now = new Date().toISOString(); + + await this.ensureSourcePathCache(); + const isImported = this.sourcePathCache.has(meta.id); + + if (isImported) { + // ---- Imported project: write to local config only ---- + const local = (await this.getLocalConfig(meta.id)) ?? {}; + local.databaseId = undefined; + await this.saveLocalConfig(meta.id, local); + } else { + // ---- Regular project: update relwave.json ---- + const updated: ProjectMetadata = { + ...meta, + databaseId: null, + status: "unlinked", + updatedAt: now, + }; + await this.writeJSON( + this.projectFile(meta.id, PROJECT_FILES.metadata), + updated + ); + } + + // Sync the index entry + const index = await this.loadIndex(); + const entry = index.projects.find((p) => p.id === meta.id); + if (entry) { + entry.databaseId = null; + entry.status = "unlinked"; + entry.updatedAt = now; + await this.saveIndex(index); + } + } + + /** + * Relink an unlinked project to a new connection. + * Throws an error if the new connection is already linked to another project. + */ + async relinkDatabase(projectId: string, databaseId: string): Promise { + // 1. Verify connection is not already linked to another project + const existingLinkedProject = await this.getProjectByDatabaseId(databaseId); + if (existingLinkedProject && existingLinkedProject.id !== projectId) { + throw new Error(`DATABASE_ALREADY_HAS_PROJECT: This connection is already linked to project "${existingLinkedProject.name}"`); + } + + // 2. Link database + const updated = await this.linkDatabase(projectId, databaseId); + if (!updated) { + throw new Error(`Project ${projectId} not found`); + } + + const now = new Date().toISOString(); + + // 3. Mark project as active again + await this.ensureSourcePathCache(); + const isImported = this.sourcePathCache.has(projectId); + + if (!isImported) { + const finalUpdated: ProjectMetadata = { + ...updated, + status: "active", + updatedAt: now, + }; + await this.writeJSON( + this.projectFile(projectId, PROJECT_FILES.metadata), + finalUpdated + ); + } + + // Sync the index entry + const index = await this.loadIndex(); + const entry = index.projects.find((p) => p.id === projectId); + if (entry) { + entry.status = "active"; + entry.updatedAt = now; + await this.saveIndex(index); + } + + return { ...updated, status: "active", updatedAt: now }; + } + /** * Delete a project. * For regular projects the internal directory is removed. * For imported projects the source directory is NOT deleted * (the user owns it) — only the index entry is removed. */ + /** + * Resolve the migrations directory for a database. + * Moves legacy migrations from AppData to the project directory if they exist. + */ + async resolveMigrationsDir(dbId: string): Promise { + const { CONFIG_FOLDER } = await import("../utils/config"); + const fallbackDir = path.join(CONFIG_FOLDER, "migrations", dbId); + let targetDir = fallbackDir; + + const project = await this.getProjectByDatabaseId(dbId); + if (project) { + targetDir = path.join(this.projectDir(project.id), "migrations"); + } + + // Migrate existing directory + if (targetDir !== fallbackDir && fsSync.existsSync(fallbackDir)) { + if (!fsSync.existsSync(targetDir)) { + await fs.mkdir(path.dirname(targetDir), { recursive: true }).catch(() => {}); + try { + await fs.rename(fallbackDir, targetDir); + } catch (e) { + console.error("Failed to migrate existing migrations dir:", e); + } + } + } + + if (!fsSync.existsSync(targetDir)) { + await fs.mkdir(targetDir, { recursive: true }).catch(() => {}); + } + + return targetDir; + } + + async analyzeImportedProject(projectId: string): Promise<{ hasLock: boolean; hashMatch: boolean; migrationsCount: number }> { + const schemaFile = await this.getSchema(projectId); + const schemaHash = (schemaFile as any)?.schemaHash || ""; + + // Read lock file + let hasLock = false; + let hashMatch = false; + let migrationsCount = 0; + + try { + const project = await this.getProject(projectId); + if (project?.databaseId) { + const { readMigrationLock } = await import("./migrationLock"); + const lock = await readMigrationLock(project.databaseId); + if (lock) { + hasLock = true; + hashMatch = lock.schemaHash === schemaHash; + migrationsCount = lock.appliedMigrations.length; + } + } + } catch (e) { + // Ignore if lock file logic fails + } + + return { hasLock, hashMatch, migrationsCount }; + } + async deleteProject(projectId: string): Promise { await this.ensureSourcePathCache(); const isImported = this.sourcePathCache.has(projectId); @@ -591,34 +798,107 @@ export class ProjectStore { await this.saveIndex(index); } + /** + * Used by the connection deletion flow when user opts to "Delete project as well". + * Finds the project linked to the databaseId, deletes its folder, and removes from the store. + */ + async deleteProjectByDatabaseId(databaseId: string): Promise { + const project = await this.getProjectByDatabaseId(databaseId); + if (!project) throw new Error("No linked project found"); + + await this.ensureSourcePathCache(); + const isImported = this.sourcePathCache.has(project.id); + + if (!isImported) { + // Transaction-like filesystem deletion first + const dir = this.projectDir(project.id); + if (fsSync.existsSync(dir)) { + await fs.rm(dir, { recursive: true, force: true }); + } + } + + // Remove from index + cache + this.sourcePathCache.delete(project.id); + const index = await this.loadIndex(); + index.projects = index.projects.filter((p) => p.id !== project.id); + await this.saveIndex(index); + } + async getSchema(projectId: string): Promise { - return this.readJSON( + const file = await this.readJSON( this.projectFile(projectId, PROJECT_FILES.schema) ); + if (!file) return null; + return this.migrateSchemaFile(file); } - async saveSchema(projectId: string, schemas: SchemaSnapshot[]): Promise { + private migrateSchemaFile(raw: any): SchemaFile { + if (raw.version >= 2) return raw as SchemaFile; + + // Upgrade v1 to v2 + return { + version: 2, + projectId: raw.projectId, + databaseId: raw.databaseId, + dialect: "postgresql", // assume postgresql for legacy files + cachedAt: raw.cachedAt || new Date().toISOString(), + relwaveVersion: "1.0.0", + schemaHash: "", // will be recomputed on next refresh + schemas: (raw.schemas || []).map((s: any) => ({ + name: s.name, + tables: (s.tables || []).map((t: any) => ({ + name: t.name, + type: t.type || "BASE TABLE", + comment: null, + columns: (t.columns || []).map((c: any, index: number) => { + const isSerial = c.defaultValue ? c.defaultValue.includes("nextval") : false; + return { + ...c, + isSerial, + ordinalPosition: index + 1, + }; + }), + indexes: [], + foreignKeys: [], + checks: [], + })), + enums: [], + views: [], + })), + }; + } + + async saveSchema( + projectId: string, + schemas: SchemaSnapshot[], + schemaHash?: string, + dialect?: string + ): Promise { const meta = await this.getProject(projectId); if (!meta) throw new Error(`Project ${projectId} not found`); // Read existing file and skip write if schema data is identical // (avoids cachedAt churn that creates phantom git changes) const existing = await this.getSchema(projectId); - if (existing) { + if (existing && existing.version === 2) { const oldData = JSON.stringify(existing.schemas); const newData = JSON.stringify(schemas); - if (oldData === newData) { + // If schemaHash is provided, also check if it matches + if (oldData === newData && (!schemaHash || existing.schemaHash === schemaHash)) { return existing; // nothing changed — keep old cachedAt } } const now = new Date().toISOString(); const file: SchemaFile = { - version: 1, + version: 2, projectId, databaseId: meta.databaseId, + dialect: (dialect || (meta.engine === "postgresql" ? "postgresql" : (meta.engine === "mysql" || meta.engine === "mariadb") ? "mysql" : "sqlite")) as any, schemas, cachedAt: now, + relwaveVersion: "1.0.0", + schemaHash: schemaHash || "", }; await this.writeJSON( @@ -1222,7 +1502,7 @@ export class ProjectStore { // ---- 3. Resolve engine from the linked database ---- let engine: string | undefined; try { - const db: DBMeta | null = await dbStoreInstance.getDB(databaseId); + const db: DBMeta | undefined = await dbStoreInstance.getDB(databaseId); engine = db?.type; } catch { // db may not exist yet diff --git a/bridge/src/services/queryExecutor.ts b/bridge/src/services/queryExecutor.ts index e098e61..425d58a 100644 --- a/bridge/src/services/queryExecutor.ts +++ b/bridge/src/services/queryExecutor.ts @@ -39,7 +39,12 @@ function mapColumn(col: any) { isPrimaryKey: col.is_primary_key === true, isForeignKey: col.is_foreign_key === true, defaultValue: col.default_value || null, - isUnique: false, + isUnique: col.is_unique === true, + isSerial: col.is_serial === true, + checkConstraint: col.check_constraint || null, + comment: col.comment || null, + maxLength: col.max_length || null, + ordinalPosition: col.ordinal_position || 0, }; } diff --git a/bridge/src/types/README.md b/bridge/src/types/README.md new file mode 100644 index 0000000..5722e12 --- /dev/null +++ b/bridge/src/types/README.md @@ -0,0 +1,28 @@ +# Types (Bridge) + +This folder contains TypeScript types shared across bridge handlers, services, connectors and query result mapping. + +Layout +- `index.ts` - central export plus `DBType`, `Rpc`, `DatabaseConfig` and `QueryParams`. +- `common.ts` - shared database metadata shapes such as tables, columns, keys, indexes, constraints, stats, migrations and SSH config. +- `postgres.ts` - PostgreSQL config and PostgreSQL-specific metadata/DDL types. +- `mysql.ts` - MySQL and MariaDB config and metadata types. +- `sqlite.ts` - SQLite config and metadata types. +- `cache.ts` - cache entry and TTL types used by connector caches. +- `ai.ts` - AI provider, prompt, result, history and error types. + +How it fits +- Connectors use these types for database-specific configs and normalized result shapes. +- Services use these types to avoid duplicating payload contracts. +- Handlers should return data that matches these types where possible, then frontend bridge services mirror the same shape. + +How to add or change types +1. Prefer extending `common.ts` when a shape applies across database engines. +2. Use database-specific files only for engine-specific fields or operations. +3. Re-export new public types from `index.ts`. +4. Update connector and frontend types together when changing RPC response shapes. + +Notes +- Keep these files type-only. Runtime helpers belong in `src/utils` or `src/services`. +- Avoid widening everything to `any`; use explicit optional fields when a database can omit metadata. +- Be careful with credential-bearing types. Public response shapes should not include plaintext passwords. diff --git a/bridge/src/types/ai.ts b/bridge/src/types/ai.ts new file mode 100644 index 0000000..7246326 --- /dev/null +++ b/bridge/src/types/ai.ts @@ -0,0 +1,96 @@ +// ── Shared types for the AI integration layer (moved from src/ai/ai.types.ts) + +export type AIProviderName = + | "anthropic" + | "openai" + | "gemini" + | "groq" + | "mistral" + | "ollama"; + +/** + * User-facing AI settings — stored client-side and passed on every RPC call. + * The bridge is stateless regarding API keys. + */ +export interface AISettings { + defaultProvider: AIProviderName; + anthropicApiKey?: string; + openaiApiKey?: string; + geminiApiKey?: string; + groqApiKey?: string; + mistralApiKey?: string; + ollamaBaseUrl?: string; + ollamaModel?: string; +} + +// ── Feature input/output types ──────────────────────────────────────────── + +export interface SchemaColumn { + name: string; + type: string; + nullable?: boolean; + isPrimaryKey?: boolean; + isForeignKey?: boolean; + references?: { table: string; column: string }; +} + +export interface SchemaTable { + name: string; + schema?: string; + columns: SchemaColumn[]; + indexes?: string[]; + foreignKeys?: string[]; + constraints?: string[]; +} + +export interface SchemaAnalysisInput { + tables: SchemaTable[]; + databaseType?: string; +} + +export interface QueryExplanationInput { + sql: string; + schema?: SchemaTable[]; + databaseType?: string; +} + +export interface ChartRecommendationInput { + tableName: string; + columns: Array<{ + name: string; + type: string; + isPrimaryKey?: boolean; + sampleValues?: string[]; + }>; +} + +export interface ChartRecommendation { + chartType: "bar" | "line" | "area" | "pie"; + xAxis: string; + yAxis: string; + reasoning: string; +} + +// ── RPC param shapes ───────────────────────────────────────────────────── + +export interface AIRequestBase { + settings: AISettings; + /** Human-readable database / datasource name for history display. */ + datasourceName?: string; + /** Table name context (e.g. for chart recommendation). */ + tableName?: string; +} + +export interface AIAnalyzeSchemaParams extends AIRequestBase { + input: SchemaAnalysisInput; +} + +export interface AIExplainQueryParams extends AIRequestBase { + input: QueryExplanationInput; +} + +export interface AIRecommendChartParams extends AIRequestBase { + input: ChartRecommendationInput; +} + +export interface AITestConnectionParams extends AIRequestBase {} diff --git a/bridge/src/types/common.ts b/bridge/src/types/common.ts index 79d6aaf..599a834 100644 --- a/bridge/src/types/common.ts +++ b/bridge/src/types/common.ts @@ -47,6 +47,10 @@ export type ColumnDetail = { is_foreign_key: boolean; ordinal_position?: number; max_length?: number | null; + is_unique?: boolean; + is_serial?: boolean; + check_constraint?: string; + comment?: string | null; }; /** diff --git a/bridge/src/types/index.ts b/bridge/src/types/index.ts index 8c5e236..dcfa1e9 100644 --- a/bridge/src/types/index.ts +++ b/bridge/src/types/index.ts @@ -14,6 +14,7 @@ export * from './common'; export * from './mysql'; export * from './postgres'; export * from './sqlite'; +export * from './ai'; export { SSHConfig } from './common'; export enum DBType { diff --git a/bridge/src/utils/README.md b/bridge/src/utils/README.md new file mode 100644 index 0000000..d5a2392 --- /dev/null +++ b/bridge/src/utils/README.md @@ -0,0 +1,27 @@ +# Utilities (Bridge) + +This folder contains small, reusable bridge helpers for paths, database type detection, migration file handling and configuration. + +Layout +- `config.ts` - RelWave data directories and file paths. Honors `RELWAVE_HOME` when set. +- `dbTypeDetector.ts` - maps saved database records to `DBType`. +- `sqlitePath.ts` - SQLite path normalization and resolution helpers. +- `migrationGenerator.ts` - SQL migration file generation for create, alter and drop flows. +- `migrationFileReader.ts` - migration file parsing/reading helpers. +- `baselineMigration.ts` - local baseline migration loading and writing helpers. + +How it fits +- Services and connectors use these helpers for filesystem paths, database type decisions and migration workflows. +- `config.ts` is the source of truth for bridge persistence locations, including databases, projects, migrations and connection folders. +- Migration utilities are used by migration handlers and connectors to generate, read and reconcile local migration files. + +How to add a utility +1. Add utilities here only when they are reusable and do not own business state. +2. Keep functions deterministic where possible and pass dependencies in as parameters. +3. Use `config.ts` path helpers for filesystem locations instead of hardcoding OS-specific paths. +4. Add tests for parsing, path resolution or SQL generation behavior that can regress quietly. + +Notes +- Utilities should not send JSON-RPC responses or import frontend code. +- Keep database-specific SQL in `src/queries`; use utility helpers for generation/parsing logic that is shared by workflows. +- Avoid storing secrets in files managed by utility paths. Credentials should go through `keyringService`. diff --git a/bridge/src/utils/baselineMigration.ts b/bridge/src/utils/baselineMigration.ts index 03d3867..291363e 100644 --- a/bridge/src/utils/baselineMigration.ts +++ b/bridge/src/utils/baselineMigration.ts @@ -1,14 +1,124 @@ import fs from "fs"; import path from "path"; -function generateBaselineSQL(version: string, name: string) { - return `-- ${version}_${name}.sql +import { SchemaFile } from "../services/projectStore"; + +function quoteIdent(name: string, dbType: string): string { + if (dbType === "mysql" || dbType === "mariadb") { + return `\`${name.replace(/`/g, "``")}\``; + } else { + return `"${name.replace(/"/g, '""')}"`; + } +} + +export function generateBaselineSQL(snapshot: SchemaFile, version: string, name: string) { + const dbType = snapshot.dialect || "unknown"; + + if (dbType === "unknown") { + return `-- ${version}_${name}.sql -- +up -- Baseline migration -- Existing schema assumed to be correct -- No-op +-- +down +-- Rollback not supported for baseline +`; + } + + let upSQL = ""; + + for (const schema of snapshot.schemas) { + // Enums (Postgres) + if (schema.enums && dbType === "postgresql") { + for (const e of schema.enums) { + const vals = e.values.map(v => `'${v.replace(/'/g, "''")}'`).join(", "); + upSQL += `CREATE TYPE ${quoteIdent(schema.name, dbType)}.${quoteIdent(e.name, dbType)} AS ENUM (${vals});\n`; + } + if (schema.enums.length > 0) upSQL += "\n"; + } + + // Tables + for (const table of schema.tables) { + // Skip internal migration tracking tables + if (["schema_migrations", "__relwave_migrations", "relwave_migrations"].includes(table.name)) { + continue; + } + + const tableRef = dbType === "sqlite" ? quoteIdent(table.name, dbType) : `${quoteIdent(schema.name, dbType)}.${quoteIdent(table.name, dbType)}`; + + // Check for sequences first (Postgres) + if (dbType === "postgresql") { + for (const col of table.columns) { + if (col.defaultValue && col.defaultValue.includes("nextval(")) { + // Extract 'windows_store_apps_id_seq' from nextval('windows_store_apps_id_seq'::regclass) + const seqMatch = col.defaultValue.match(/nextval\('([^']+)'/i) || col.defaultValue.match(/nextval\("([^"]+)"/i); + if (seqMatch && seqMatch[1]) { + const seqName = seqMatch[1]; + const isSchemaQualified = seqName.includes("."); + const seqRef = isSchemaQualified ? seqName : `${quoteIdent(schema.name, dbType)}.${quoteIdent(seqName, dbType)}`; + upSQL += `CREATE SEQUENCE IF NOT EXISTS ${seqRef};\n`; + } + } + } + } + + const columnDefs = table.columns.map(col => { + let def = ` ${quoteIdent(col.name, dbType)} ${col.type}`; + if (!col.nullable) def += " NOT NULL"; + if (col.defaultValue) def += ` DEFAULT ${col.defaultValue}`; + if (col.isPrimaryKey) def += " PRIMARY KEY"; + if (col.isUnique && !col.isPrimaryKey) def += " UNIQUE"; + if (col.checkConstraint) def += ` ${col.checkConstraint}`; + return def; + }); + + // check constraints + if (table.checks && table.checks.length > 0) { + for (const chk of table.checks) { + columnDefs.push(` CONSTRAINT ${quoteIdent(chk.name, dbType)} CHECK (${chk.expression})`); + } + } + + const fkDefs = (table.foreignKeys || []).map(fk => { + const onDelete = fk.onDelete || "NO ACTION"; + const onUpdate = fk.onUpdate || "NO ACTION"; + const targetRef = dbType === "sqlite" ? quoteIdent(fk.referencedTable, dbType) : `${quoteIdent(fk.referencedSchema || schema.name, dbType)}.${quoteIdent(fk.referencedTable, dbType)}`; + const srcCols = fk.columns.map(c => quoteIdent(c, dbType)).join(", "); + const tgtCols = fk.referencedColumns.map(c => quoteIdent(c, dbType)).join(", "); + return ` CONSTRAINT ${quoteIdent(fk.name, dbType)} FOREIGN KEY (${srcCols}) REFERENCES ${targetRef}(${tgtCols}) ON DELETE ${onDelete} ON UPDATE ${onUpdate}`; + }); + + const allDefs = [...columnDefs, ...fkDefs].join(",\n"); + + upSQL += `CREATE TABLE ${tableRef} (\n${allDefs}\n);\n\n`; + + // Indexes + const indexesToCreate = (table.indexes || []); + for (const idx of indexesToCreate) { + const idxCols = idx.columns.map(c => quoteIdent(c, dbType)).join(", "); + const uniqueStr = idx.unique ? "UNIQUE " : ""; + upSQL += `CREATE ${uniqueStr}INDEX ${quoteIdent(idx.name, dbType)} ON ${tableRef} (${idxCols});\n`; + } + if (indexesToCreate.length > 0) upSQL += "\n"; + } + + // Views + if (schema.views) { + for (const v of schema.views) { + const viewRef = dbType === "sqlite" ? quoteIdent(v.name, dbType) : `${quoteIdent(schema.name, dbType)}.${quoteIdent(v.name, dbType)}`; + upSQL += `CREATE VIEW ${viewRef} AS ${v.definition};\n\n`; + } + } + } + + return `-- ${version}_${name}.sql + +-- +up +-- Baseline migration generated from current schema snapshot +${upSQL.trim()} + -- +down -- Rollback not supported for baseline `; @@ -18,7 +128,8 @@ function generateBaselineSQL(version: string, name: string) { export function writeBaselineMigration( migrationsDir: string, version: string, - name: string + name: string, + snapshot: SchemaFile ) { if (!fs.existsSync(migrationsDir)) { fs.mkdirSync(migrationsDir, { recursive: true }); @@ -27,7 +138,7 @@ export function writeBaselineMigration( const filename = `${version}_${name}.sql`; const filepath = path.join(migrationsDir, filename); - const sql = generateBaselineSQL(version, name); + const sql = generateBaselineSQL(snapshot, version, name); fs.writeFileSync(filepath, sql, "utf8"); return filepath; diff --git a/bridge/src/utils/config.ts b/bridge/src/utils/config.ts index c1435ec..f650257 100644 --- a/bridge/src/utils/config.ts +++ b/bridge/src/utils/config.ts @@ -22,10 +22,6 @@ export function getConnectionDir(connectionId: string) { return path.join(CONFIG_FOLDER, "connections", connectionId); } -export function getMigrationsDir(connectionId: string) { - return path.join(CONFIG_FOLDER, "migrations", connectionId); -} - export function getProjectDir(projectId: string) { return path.join(PROJECTS_FOLDER, projectId); } diff --git a/bridge/src/utils/migrationFileReader.ts b/bridge/src/utils/migrationFileReader.ts index ac5a07e..88627fc 100644 --- a/bridge/src/utils/migrationFileReader.ts +++ b/bridge/src/utils/migrationFileReader.ts @@ -17,8 +17,8 @@ export function readMigrationFile(filepath: string): ParsedMigration { const content = fs.readFileSync(filepath, "utf8"); const filename = path.basename(filepath); - // Extract version and name from filename: 20260104160000_create_users.sql - const match = filename.match(/^(\d{14})_(.+)\.sql$/); + // Accept both 14-digit (YYYYMMDDHHmmss) and 13-digit (Date.now() ms) version formats + const match = filename.match(/^(\d{13,14})_(.+)\.sql$/); if (!match) { throw new Error(`Invalid migration filename format: ${filename}`); } diff --git a/index.html b/index.html index 71d3362..84ec270 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,17 @@ - + - - - - - + + + + + RelWave diff --git a/package.json b/package.json index 755d04c..7b50937 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "relwave", "private": true, - "version": "0.7.0-beta-1", + "version": " 0.9.0-rc-1", "type": "module", "scripts": { "dev": "vite", @@ -53,6 +53,7 @@ "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.9.6", "reactflow": "^11.11.4", "recharts": "^2.15.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9048824..8d58629 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.8)(react@19.2.3) react-router-dom: specifier: ^7.9.6 version: 7.9.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1944,21 +1947,36 @@ packages: '@types/dagre@0.7.53': resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.12.0': resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} @@ -1973,6 +1991,12 @@ packages: '@types/stylis@4.2.5': resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -1998,6 +2022,9 @@ packages: react: '>=17.0.0' react-dom: '>=17.0.0' + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -2027,6 +2054,9 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -2042,10 +2072,25 @@ packages: caniuse-lite@1.0.30001756: resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2079,6 +2124,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concurrently@9.2.1: resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} engines: {node: '>=18'} @@ -2191,6 +2239,13 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2198,6 +2253,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -2228,9 +2286,15 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-equals@5.4.0: resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} engines: {node: '>=6.0.0'} @@ -2275,23 +2339,51 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hotkeys-js@3.13.15: resolution: {integrity: sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==} html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2413,6 +2505,9 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2436,9 +2531,96 @@ packages: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2463,6 +2645,9 @@ packages: orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2484,6 +2669,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + prosemirror-changeset@2.4.0: resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} @@ -2576,6 +2764,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -2659,6 +2853,12 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2701,10 +2901,16 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2712,6 +2918,12 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-components@6.1.19: resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==} engines: {node: '>= 16'} @@ -2757,6 +2969,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -2777,6 +2995,24 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -2808,6 +3044,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} @@ -2888,6 +3130,9 @@ packages: react: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@babel/code-frame@7.27.1': @@ -4693,10 +4938,22 @@ snapshots: '@types/dagre@0.7.53': {} + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/linkify-it@5.0.0': {} '@types/markdown-it@14.1.2': @@ -4704,8 +4961,14 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} + '@types/node@24.12.0': dependencies: undici-types: 7.16.0 @@ -4720,6 +4983,10 @@ snapshots: '@types/stylis@4.2.5': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)': @@ -4749,6 +5016,8 @@ snapshots: - '@codemirror/lint' - '@codemirror/search' + '@ungap/structured-clone@1.3.1': {} + '@use-gesture/core@10.3.1': {} '@use-gesture/react@10.3.1(react@19.2.3)': @@ -4780,6 +5049,8 @@ snapshots: dependencies: tslib: 2.8.1 + bail@2.0.2: {} + baseline-browser-mapping@2.9.19: {} browserslist@4.28.0: @@ -4794,11 +5065,21 @@ snapshots: caniuse-lite@1.0.30001756: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -4843,6 +5124,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + concurrently@9.2.1: dependencies: chalk: 4.1.2 @@ -4947,10 +5230,20 @@ snapshots: decimal.js-light@2.5.1: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -5000,8 +5293,12 @@ snapshots: escape-string-regexp@4.0.0: {} + estree-util-is-identifier-name@3.0.0: {} + eventemitter3@4.0.7: {} + extend@3.0.2: {} + fast-equals@5.4.0: {} fdir@6.5.0(picomatch@4.0.3): @@ -5027,16 +5324,57 @@ snapshots: has-flag@4.0.0: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hotkeys-js@3.13.15: {} html-to-image@1.11.13: {} + html-url-attributes@3.0.1: {} + idb@7.1.1: {} + inline-style-parser@0.2.7: {} + internmap@2.0.3: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + is-fullwidth-code-point@3.0.0: {} + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} jiti@2.6.1: {} @@ -5116,6 +5454,8 @@ snapshots: lodash@4.17.23: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5143,8 +5483,230 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + ms@2.1.3: {} nanoid@3.3.11: {} @@ -5160,6 +5722,16 @@ snapshots: orderedmap@2.1.1: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -5184,6 +5756,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + prosemirror-changeset@2.4.0: dependencies: prosemirror-transform: 1.11.0 @@ -5367,6 +5941,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@19.2.8)(react@19.2.3): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.8 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): @@ -5460,6 +6052,23 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + require-directory@2.1.1: {} rollup@4.59.0: @@ -5516,18 +6125,33 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 style-mod@4.1.3: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-components@6.1.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@emotion/is-prop-valid': 1.2.2 @@ -5590,6 +6214,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@2.6.2: {} tslib@2.8.1: {} @@ -5602,6 +6230,39 @@ snapshots: undici-types@7.16.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0 @@ -5627,6 +6288,16 @@ snapshots: dependencies: react: 19.2.3 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.2 @@ -5688,3 +6359,5 @@ snapshots: optionalDependencies: '@types/react': 19.2.8 react: 19.2.3 + + zwitch@2.0.4: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ed0def2..2b42f61 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3073,7 +3073,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relwave" -version = "0.7.0-beta-1" +version = " 0.9.0-rc-1" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 64d5cec..3d3c9be 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "relwave" -version = "0.7.0-beta-1" +version = " 0.9.0-rc-1" description = "A powerful, cross-platform desktop application for database management with native Git version control." authors = ["yashh56"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bc0ac2f..bebb698 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "RelWave", - "version": "0.7.0-beta-1", + "version": " 0.9.0-rc-1", "identifier": "tech.relwave.app", "build": { "beforeDevCommand": "vite", diff --git a/src/App.css b/src/App.css index 125d27c..8e5d813 100644 --- a/src/App.css +++ b/src/App.css @@ -3,10 +3,21 @@ @custom-variant dark (&:is(.dark *)); +.font-sans { + font-family: "IBM Plex Sans", sans-serif; +} + +.font-mono { + font-family: "IBM Plex Mono", monospace; +} + /* Font Family Configuration */ @theme inline { - --font-sans: "Geist", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-mono: "Geist Mono", "JetBrains Mono", "Fira Code", ui-monospace, monospace; + --font-sans: + "Geist", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + --font-mono: + "Geist Mono", "JetBrains Mono", "Fira Code", ui-monospace, monospace; } @theme inline { @@ -80,9 +91,10 @@ --sidebar-accent-foreground: oklch(0.22 0.02 252); --sidebar-border: oklch(0.875 0.012 247); --sidebar-ring: var(--ring); - + /* New Design System Tokens */ - --card-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 32px rgba(15, 23, 42, 0.05); + --card-shadow: + 0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 32px rgba(15, 23, 42, 0.05); --card-shadow-hover: 0 12px 36px rgba(15, 23, 42, 0.1); --glass-bg: rgba(255, 255, 255, 0.78); --glass-blur: 12px; @@ -138,8 +150,10 @@ body, --sidebar-accent-foreground: oklch(0.94 0.006 255); --sidebar-border: oklch(1 0 0 / 9%); --sidebar-ring: var(--ring); - --card-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 18px 52px rgba(0, 0, 0, 0.34); - --card-shadow-hover: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 22px 64px rgba(0, 0, 0, 0.42); + --card-shadow: + 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 18px 52px rgba(0, 0, 0, 0.34); + --card-shadow-hover: + 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 22px 64px rgba(0, 0, 0, 0.42); --glass-bg: rgba(23, 29, 39, 0.76); --surface: oklch(0.16 0.014 250); --surface-elevated: oklch(0.19 0.016 250); @@ -203,230 +217,242 @@ body, tricks. */ [data-theme-variant="cyberpunk"], .dark[data-theme-variant="cyberpunk"] { - --background: #070b14; - --foreground: #e2f0ff; - --card: #0d1526; - --card-foreground: #e2f0ff; - --popover: #0d1526; - --popover-foreground: #e2f0ff; - --primary: #00f5ff; - --primary-foreground: #030c12; - --secondary: #0f1e35; - --secondary-foreground: #a8cfef; - --muted: #0d1a2e; - --muted-foreground: #6a8faf; - --accent: #ff00c8; - --accent-foreground: #fff; - --destructive: #ff3366; - --border: #0e2540; - --input: #0a1a2e; - --ring: #00f5ff; - --sidebar: #060d1c; - --sidebar-foreground: #cde8ff; - --sidebar-primary: #00f5ff; + --background: #070b14; + --foreground: #e2f0ff; + --card: #0d1526; + --card-foreground: #e2f0ff; + --popover: #0d1526; + --popover-foreground: #e2f0ff; + --primary: #00f5ff; + --primary-foreground: #030c12; + --secondary: #0f1e35; + --secondary-foreground: #a8cfef; + --muted: #0d1a2e; + --muted-foreground: #6a8faf; + --accent: #ff00c8; + --accent-foreground: #fff; + --destructive: #ff3366; + --border: #0e2540; + --input: #0a1a2e; + --ring: #00f5ff; + --sidebar: #060d1c; + --sidebar-foreground: #cde8ff; + --sidebar-primary: #00f5ff; --sidebar-primary-foreground: #030c12; - --sidebar-accent: #0f2240; + --sidebar-accent: #0f2240; --sidebar-accent-foreground: #00f5ff; - --sidebar-border: rgba(0,245,255,0.08); - --sidebar-ring: #00f5ff; - --card-shadow: 0 0 0 1px rgba(0,245,255,0.06), 0 8px 32px rgba(0,0,0,0.6); - --card-shadow-hover: 0 0 0 1px rgba(0,245,255,0.14), 0 12px 44px rgba(0,0,0,0.7); - --glass-bg: rgba(7,11,20,0.85); - --surface: #0a1220; - --surface-elevated: #0d1526; - --surface-subtle: #0f1d32; - --border-subtle: rgba(0,245,255,0.07); + --sidebar-border: rgba(0, 245, 255, 0.08); + --sidebar-ring: #00f5ff; + --card-shadow: + 0 0 0 1px rgba(0, 245, 255, 0.06), 0 8px 32px rgba(0, 0, 0, 0.6); + --card-shadow-hover: + 0 0 0 1px rgba(0, 245, 255, 0.14), 0 12px 44px rgba(0, 0, 0, 0.7); + --glass-bg: rgba(7, 11, 20, 0.85); + --surface: #0a1220; + --surface-elevated: #0d1526; + --surface-subtle: #0f1d32; + --border-subtle: rgba(0, 245, 255, 0.07); } /* Cyberpunk neon glow on focused/primary interactive elements */ -[data-theme-variant="cyberpunk"] button[data-slot="button"]:not([data-variant="ghost"]):not([data-variant="outline"]):focus-visible, +[data-theme-variant="cyberpunk"] + button[data-slot="button"]:not([data-variant="ghost"]):not( + [data-variant="outline"] + ):focus-visible, [data-theme-variant="cyberpunk"] [role="button"]:focus-visible { - box-shadow: 0 0 0 2px #00f5ff, 0 0 12px rgba(0,245,255,0.4); + box-shadow: + 0 0 0 2px #00f5ff, + 0 0 12px rgba(0, 245, 255, 0.4); } /* ── Full-palette theme: VS Code ──────────────────────────────────────── */ [data-theme-variant="vscode"], .dark[data-theme-variant="vscode"] { - --background: #1e1e1e; - --foreground: #d4d4d4; - --card: #252526; - --card-foreground: #d4d4d4; - --popover: #2d2d30; - --popover-foreground: #d4d4d4; - --primary: #569cd6; - --primary-foreground: #1e1e1e; - --secondary: #2d2d30; - --secondary-foreground: #cccccc; - --muted: #333333; - --muted-foreground: #808080; - --accent: #264f78; - --accent-foreground: #d4d4d4; - --destructive: #f44747; - --border: #3e3e42; - --input: #3c3c3c; - --ring: #569cd6; - --sidebar: #252526; - --sidebar-foreground: #bbbbbb; - --sidebar-primary: #569cd6; + --background: #1e1e1e; + --foreground: #d4d4d4; + --card: #252526; + --card-foreground: #d4d4d4; + --popover: #2d2d30; + --popover-foreground: #d4d4d4; + --primary: #569cd6; + --primary-foreground: #1e1e1e; + --secondary: #2d2d30; + --secondary-foreground: #cccccc; + --muted: #333333; + --muted-foreground: #808080; + --accent: #264f78; + --accent-foreground: #d4d4d4; + --destructive: #f44747; + --border: #3e3e42; + --input: #3c3c3c; + --ring: #569cd6; + --sidebar: #252526; + --sidebar-foreground: #bbbbbb; + --sidebar-primary: #569cd6; --sidebar-primary-foreground: #1e1e1e; - --sidebar-accent: #37373d; + --sidebar-accent: #37373d; --sidebar-accent-foreground: #ffffff; - --sidebar-border: #3e3e42; - --sidebar-ring: #569cd6; - --card-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px rgba(0,0,0,0.4); - --card-shadow-hover: 0 1px 0 rgba(255,255,255,0.06) inset, 0 12px 32px rgba(0,0,0,0.5); - --glass-bg: rgba(37,37,38,0.92); - --surface: #252526; - --surface-elevated: #2d2d30; - --surface-subtle: #333333; - --border-subtle: #3e3e42; + --sidebar-border: #3e3e42; + --sidebar-ring: #569cd6; + --card-shadow: + 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 8px 24px rgba(0, 0, 0, 0.4); + --card-shadow-hover: + 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 12px 32px rgba(0, 0, 0, 0.5); + --glass-bg: rgba(37, 37, 38, 0.92); + --surface: #252526; + --surface-elevated: #2d2d30; + --surface-subtle: #333333; + --border-subtle: #3e3e42; } /* ── Full-palette theme: Valorant ────────────────────────────────────── */ [data-theme-variant="valorant"] { - --background: oklch(0.97 0.02 12.78); - --foreground: oklch(0.24 0.07 17.81); - --card: oklch(0.98 0.01 17.28); - --card-foreground: oklch(0.26 0.07 19); - --popover: oklch(0.98 0.01 17.28); - --popover-foreground: oklch(0.26 0.07 19); - --primary: oklch(0.67 0.22 21.34); - --primary-foreground: oklch(0.99 0.00 359.99); - --secondary: oklch(0.95 0.02 11.28); - --secondary-foreground: oklch(0.24 0.07 17.81); - --muted: oklch(0.98 0.01 17.28); - --muted-foreground: oklch(0.55 0.08 19); - --accent: oklch(0.99 0.00 359.99); - --accent-foreground: oklch(0.43 0.13 20.62); - --destructive: oklch(0.80 0.17 73.27); - --border: oklch(0.91 0.05 11.40); - --input: oklch(0.90 0.05 12.59); - --ring: oklch(0.92 0.04 12.39); - --sidebar: oklch(0.97 0.02 12.78); - --sidebar-foreground: oklch(0.26 0.07 19); - --sidebar-primary: oklch(0.67 0.22 21.34); - --sidebar-primary-foreground: oklch(0.99 0.00 359.99); - --sidebar-accent: oklch(0.98 0.01 17.28); + --background: oklch(0.97 0.02 12.78); + --foreground: oklch(0.24 0.07 17.81); + --card: oklch(0.98 0.01 17.28); + --card-foreground: oklch(0.26 0.07 19); + --popover: oklch(0.98 0.01 17.28); + --popover-foreground: oklch(0.26 0.07 19); + --primary: oklch(0.67 0.22 21.34); + --primary-foreground: oklch(0.99 0 359.99); + --secondary: oklch(0.95 0.02 11.28); + --secondary-foreground: oklch(0.24 0.07 17.81); + --muted: oklch(0.98 0.01 17.28); + --muted-foreground: oklch(0.55 0.08 19); + --accent: oklch(0.99 0 359.99); + --accent-foreground: oklch(0.43 0.13 20.62); + --destructive: oklch(0.8 0.17 73.27); + --border: oklch(0.91 0.05 11.4); + --input: oklch(0.9 0.05 12.59); + --ring: oklch(0.92 0.04 12.39); + --sidebar: oklch(0.97 0.02 12.78); + --sidebar-foreground: oklch(0.26 0.07 19); + --sidebar-primary: oklch(0.67 0.22 21.34); + --sidebar-primary-foreground: oklch(0.99 0 359.99); + --sidebar-accent: oklch(0.98 0.01 17.28); --sidebar-accent-foreground: oklch(0.43 0.13 20.62); - --sidebar-border: oklch(0.91 0.05 11.40); - --sidebar-ring: oklch(0.92 0.04 12.39); - --card-shadow: 0px 0px 3px 0px oklch(0.3 0.0891 19.6 / 0.08), 0px 2px 4px -1px oklch(0.3 0.0891 19.6 / 0.08); - --glass-bg: rgba(253, 248, 247, 0.82); - --surface: oklch(0.97 0.02 12.78); - --surface-elevated: oklch(0.98 0.01 17.28); - --surface-subtle: oklch(0.95 0.02 11.28); - --border-subtle: oklch(0.91 0.05 11.40); + --sidebar-border: oklch(0.91 0.05 11.4); + --sidebar-ring: oklch(0.92 0.04 12.39); + --card-shadow: + 0px 0px 3px 0px oklch(0.3 0.0891 19.6 / 0.08), + 0px 2px 4px -1px oklch(0.3 0.0891 19.6 / 0.08); + --glass-bg: rgba(253, 248, 247, 0.82); + --surface: oklch(0.97 0.02 12.78); + --surface-elevated: oklch(0.98 0.01 17.28); + --surface-subtle: oklch(0.95 0.02 11.28); + --border-subtle: oklch(0.91 0.05 11.4); } .dark[data-theme-variant="valorant"] { - --background: oklch(0.16 0.03 17.48); - --foreground: oklch(0.99 0.00 359.99); - --card: oklch(0.21 0.05 19.26); - --card-foreground: oklch(0.98 0 0); - --popover: oklch(0.26 0.07 19); - --popover-foreground: oklch(0.99 0.00 359.99); - --primary: oklch(0.67 0.22 21.34); - --primary-foreground: oklch(0.99 0.00 359.99); - --secondary: oklch(0.3 0.0891 19.6); - --secondary-foreground: oklch(0.95 0.02 11.28); - --muted: oklch(0.26 0.07 19); - --muted-foreground: oklch(0.72 0.04 18); - --accent: oklch(0.43 0.13 20.62); - --accent-foreground: oklch(0.99 0.00 359.99); - --destructive: oklch(0.80 0.17 73.27); - --border: oklch(0.31 0.09 19.80); - --input: oklch(0.39 0.12 20.37); - --ring: oklch(0.50 0.16 20.89); - --sidebar: oklch(0.26 0.07 19); - --sidebar-foreground: oklch(0.99 0.00 359.99); - --sidebar-primary: oklch(0.67 0.22 21.34); - --sidebar-primary-foreground: oklch(0.99 0.00 359.99); - --sidebar-accent: oklch(0.43 0.13 20.62); - --sidebar-accent-foreground: oklch(0.99 0.00 359.99); - --sidebar-border: oklch(0.39 0.12 20.37); - --sidebar-ring: oklch(0.50 0.16 20.89); - --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.10), 0 2px 4px -1px oklch(0.00 0 0 / 0.10); - --glass-bg: rgba(33, 14, 12, 0.82); - --surface: oklch(0.21 0.05 19.26); - --surface-elevated: oklch(0.26 0.07 19); - --surface-subtle: oklch(0.3 0.0891 19.6); - --border-subtle: oklch(0.31 0.09 19.80); + --background: oklch(0.16 0.03 17.48); + --foreground: oklch(0.99 0 359.99); + --card: oklch(0.21 0.05 19.26); + --card-foreground: oklch(0.98 0 0); + --popover: oklch(0.26 0.07 19); + --popover-foreground: oklch(0.99 0 359.99); + --primary: oklch(0.67 0.22 21.34); + --primary-foreground: oklch(0.99 0 359.99); + --secondary: oklch(0.3 0.0891 19.6); + --secondary-foreground: oklch(0.95 0.02 11.28); + --muted: oklch(0.26 0.07 19); + --muted-foreground: oklch(0.72 0.04 18); + --accent: oklch(0.43 0.13 20.62); + --accent-foreground: oklch(0.99 0 359.99); + --destructive: oklch(0.8 0.17 73.27); + --border: oklch(0.31 0.09 19.8); + --input: oklch(0.39 0.12 20.37); + --ring: oklch(0.5 0.16 20.89); + --sidebar: oklch(0.26 0.07 19); + --sidebar-foreground: oklch(0.99 0 359.99); + --sidebar-primary: oklch(0.67 0.22 21.34); + --sidebar-primary-foreground: oklch(0.99 0 359.99); + --sidebar-accent: oklch(0.43 0.13 20.62); + --sidebar-accent-foreground: oklch(0.99 0 359.99); + --sidebar-border: oklch(0.39 0.12 20.37); + --sidebar-ring: oklch(0.5 0.16 20.89); + --card-shadow: + 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 2px 4px -1px oklch(0 0 0 / 0.1); + --glass-bg: rgba(33, 14, 12, 0.82); + --surface: oklch(0.21 0.05 19.26); + --surface-elevated: oklch(0.26 0.07 19); + --surface-subtle: oklch(0.3 0.0891 19.6); + --border-subtle: oklch(0.31 0.09 19.8); } /* ── Full-palette theme: Ghibli Studio ──────────────────────────────── */ [data-theme-variant="ghibli"] { - --background: oklch(0.91 0.05 82.69); - --foreground: oklch(0.41 0.08 79.04); - --card: oklch(0.92 0.04 83.86); - --card-foreground: oklch(0.41 0.08 73.75); - --popover: oklch(0.92 0.04 83.86); - --popover-foreground: oklch(0.41 0.08 73.75); - --primary: oklch(0.71 0.10 111.99); - --primary-foreground: oklch(0.98 0.01 3.71); - --secondary: oklch(0.88 0.05 83.41); - --secondary-foreground: oklch(0.51 0.08 79.21); - --muted: oklch(0.86 0.06 83.48); - --muted-foreground: oklch(0.51 0.08 74.26); - --accent: oklch(0.86 0.05 84.50); - --accent-foreground: oklch(0.26 0.02 358.42); - --destructive: oklch(0.63 0.24 29.21); - --border: oklch(0.74 0.06 79.81); - --input: oklch(0.74 0.06 79.81); - --ring: oklch(0.51 0.08 74.26); - --sidebar: oklch(0.87 0.06 83.96); - --sidebar-foreground: oklch(0.41 0.08 79.04); - --sidebar-primary: oklch(0.26 0.02 358.42); + --background: oklch(0.91 0.05 82.69); + --foreground: oklch(0.41 0.08 79.04); + --card: oklch(0.92 0.04 83.86); + --card-foreground: oklch(0.41 0.08 73.75); + --popover: oklch(0.92 0.04 83.86); + --popover-foreground: oklch(0.41 0.08 73.75); + --primary: oklch(0.71 0.1 111.99); + --primary-foreground: oklch(0.98 0.01 3.71); + --secondary: oklch(0.88 0.05 83.41); + --secondary-foreground: oklch(0.51 0.08 79.21); + --muted: oklch(0.86 0.06 83.48); + --muted-foreground: oklch(0.51 0.08 74.26); + --accent: oklch(0.86 0.05 84.5); + --accent-foreground: oklch(0.26 0.02 358.42); + --destructive: oklch(0.63 0.24 29.21); + --border: oklch(0.74 0.06 79.81); + --input: oklch(0.74 0.06 79.81); + --ring: oklch(0.51 0.08 74.26); + --sidebar: oklch(0.87 0.06 83.96); + --sidebar-foreground: oklch(0.41 0.08 79.04); + --sidebar-primary: oklch(0.26 0.02 358.42); --sidebar-primary-foreground: oklch(0.98 0.01 3.71); - --sidebar-accent: oklch(0.83 0.06 84.46); + --sidebar-accent: oklch(0.83 0.06 84.46); --sidebar-accent-foreground: oklch(0.26 0.02 358.42); - --sidebar-border: oklch(0.91 0.00 0.43); - --sidebar-ring: oklch(0.71 0.00 0.37); - --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.08), 0 2px 4px -1px oklch(0.00 0 0 / 0.08); - --glass-bg: rgba(244, 237, 216, 0.80); - --surface: oklch(0.91 0.05 82.69); - --surface-elevated: oklch(0.92 0.04 83.86); - --surface-subtle: oklch(0.88 0.05 83.41); - --border-subtle: oklch(0.74 0.06 79.81); + --sidebar-border: oklch(0.91 0 0.43); + --sidebar-ring: oklch(0.71 0 0.37); + --card-shadow: + 0 1px 3px 0px oklch(0 0 0 / 0.08), 0 2px 4px -1px oklch(0 0 0 / 0.08); + --glass-bg: rgba(244, 237, 216, 0.8); + --surface: oklch(0.91 0.05 82.69); + --surface-elevated: oklch(0.92 0.04 83.86); + --surface-subtle: oklch(0.88 0.05 83.41); + --border-subtle: oklch(0.74 0.06 79.81); } .dark[data-theme-variant="ghibli"] { - --background: oklch(0.20 0.01 48.35); - --foreground: oklch(0.88 0.05 79.26); - --card: oklch(0.25 0.01 56.14); - --card-foreground: oklch(0.88 0.05 79.26); - --popover: oklch(0.25 0.01 56.14); - --popover-foreground: oklch(0.88 0.05 79.26); - --primary: oklch(0.64 0.05 115.39); - --primary-foreground: oklch(0.98 0.01 3.71); - --secondary: oklch(0.33 0.02 60.70); - --secondary-foreground: oklch(0.88 0.05 83.41); - --muted: oklch(0.27 0.01 39.35); - --muted-foreground: oklch(0.74 0.06 79.81); - --accent: oklch(0.33 0.02 60.70); - --accent-foreground: oklch(0.86 0.05 84.50); - --destructive: oklch(0.63 0.24 29.21); - --border: oklch(0.33 0.02 60.70); - --input: oklch(0.33 0.02 60.70); - --ring: oklch(0.64 0.05 115.39); - --sidebar: oklch(0.23 0.01 56.09); - --sidebar-foreground: oklch(0.88 0.05 79.26); - --sidebar-primary: oklch(0.64 0.05 115.39); + --background: oklch(0.2 0.01 48.35); + --foreground: oklch(0.88 0.05 79.26); + --card: oklch(0.25 0.01 56.14); + --card-foreground: oklch(0.88 0.05 79.26); + --popover: oklch(0.25 0.01 56.14); + --popover-foreground: oklch(0.88 0.05 79.26); + --primary: oklch(0.64 0.05 115.39); + --primary-foreground: oklch(0.98 0.01 3.71); + --secondary: oklch(0.33 0.02 60.7); + --secondary-foreground: oklch(0.88 0.05 83.41); + --muted: oklch(0.27 0.01 39.35); + --muted-foreground: oklch(0.74 0.06 79.81); + --accent: oklch(0.33 0.02 60.7); + --accent-foreground: oklch(0.86 0.05 84.5); + --destructive: oklch(0.63 0.24 29.21); + --border: oklch(0.33 0.02 60.7); + --input: oklch(0.33 0.02 60.7); + --ring: oklch(0.64 0.05 115.39); + --sidebar: oklch(0.23 0.01 56.09); + --sidebar-foreground: oklch(0.88 0.05 79.26); + --sidebar-primary: oklch(0.64 0.05 115.39); --sidebar-primary-foreground: oklch(0.98 0.01 3.71); - --sidebar-accent: oklch(0.33 0.02 60.70); - --sidebar-accent-foreground: oklch(0.86 0.05 84.50); - --sidebar-border: oklch(0.33 0.02 60.70); - --sidebar-ring: oklch(0.64 0.05 115.39); - --card-shadow: 0 1px 3px 0px oklch(0.00 0 0 / 0.20), 0 2px 4px -1px oklch(0.00 0 0 / 0.20); - --glass-bg: rgba(33, 25, 14, 0.82); - --surface: oklch(0.25 0.01 56.14); - --surface-elevated: oklch(0.27 0.01 39.35); - --surface-subtle: oklch(0.33 0.02 60.70); - --border-subtle: oklch(0.33 0.02 60.70); + --sidebar-accent: oklch(0.33 0.02 60.7); + --sidebar-accent-foreground: oklch(0.86 0.05 84.5); + --sidebar-border: oklch(0.33 0.02 60.7); + --sidebar-ring: oklch(0.64 0.05 115.39); + --card-shadow: + 0 1px 3px 0px oklch(0 0 0 / 0.2), 0 2px 4px -1px oklch(0 0 0 / 0.2); + --glass-bg: rgba(33, 25, 14, 0.82); + --surface: oklch(0.25 0.01 56.14); + --surface-elevated: oklch(0.27 0.01 39.35); + --surface-subtle: oklch(0.33 0.02 60.7); + --border-subtle: oklch(0.33 0.02 60.7); } - @layer base { - * { @apply border-border outline-ring/50; } @@ -436,15 +462,26 @@ body, font-feature-settings: "cv02", "cv03", "cv04", "cv11"; text-rendering: optimizeLegibility; background: - radial-gradient(circle at 18% -10%, color-mix(in oklch, var(--primary) 14%, transparent) 0, transparent 32rem), - linear-gradient(180deg, color-mix(in oklch, var(--background) 96%, white), var(--background)); + radial-gradient( + circle at 18% -10%, + color-mix(in oklch, var(--primary) 14%, transparent) 0, + transparent 32rem + ), + linear-gradient( + 180deg, + color-mix(in oklch, var(--background) 96%, white), + var(--background) + ); } - + /* Apply monospace font to technical elements */ - code, pre, kbd, samp { + code, + pre, + kbd, + samp { font-family: var(--font-mono); } - + /* Ensure font-mono class uses our mono font */ .font-mono { font-family: var(--font-mono) !important; @@ -461,16 +498,7 @@ body { user-select: none; } -input, -textarea, -[contenteditable="true"], -pre, -code, -.selectable, -.font-mono { - -webkit-user-select: text; - user-select: text; -} + @layer utilities { /* Webkit browsers (Chrome, Safari, Edge) - Applied globally */ @@ -514,7 +542,11 @@ code, .app-surface { background: - linear-gradient(180deg, color-mix(in oklch, var(--surface) 92%, transparent), var(--background)), + linear-gradient( + 180deg, + color-mix(in oklch, var(--surface) 92%, transparent), + var(--background) + ), var(--background); } @@ -545,8 +577,15 @@ code, .hairline-grid { background-image: - linear-gradient(color-mix(in oklch, var(--border) 38%, transparent) 1px, transparent 1px), - linear-gradient(90deg, color-mix(in oklch, var(--border) 38%, transparent) 1px, transparent 1px); + linear-gradient( + color-mix(in oklch, var(--border) 38%, transparent) 1px, + transparent 1px + ), + linear-gradient( + 90deg, + color-mix(in oklch, var(--border) 38%, transparent) 1px, + transparent 1px + ); background-size: 28px 28px; } @@ -560,13 +599,17 @@ code, .route-transition--in { opacity: 1; transform: translateY(0); - transition: opacity 260ms cubic-bezier(0.22, 1, 0.36, 1), transform 260ms cubic-bezier(0.22, 1, 0.36, 1); + transition: + opacity 260ms cubic-bezier(0.22, 1, 0.36, 1), + transform 260ms cubic-bezier(0.22, 1, 0.36, 1); } .route-transition--out { opacity: 0.72; transform: translateY(2px); - transition: opacity 130ms cubic-bezier(0.4, 0, 1, 1), transform 130ms cubic-bezier(0.4, 0, 1, 1); + transition: + opacity 130ms cubic-bezier(0.4, 0, 1, 1), + transform 130ms cubic-bezier(0.4, 0, 1, 1); } @media (prefers-reduced-motion: reduce) { @@ -586,4 +629,35 @@ code, .dark * { scrollbar-color: rgba(156, 163, 175, 0.2) transparent; } + + .theme-transitioning *, + .theme-transitioning *::before, + .theme-transitioning *::after { + transition-duration: 0ms !important; + transition-delay: 0ms !important; + } + + .theme-transitioning [data-slot="dialog-content"], + .theme-transitioning [data-slot="dialog-content"] *, + .theme-transitioning [data-slot="dialog-content"] *::before, + .theme-transitioning [data-slot="dialog-content"] *::after { + transition-property: + color, background-color, border-color, box-shadow, opacity !important; + transition-duration: 260ms !important; + transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1) !important; + } + + /* View Transition API rules for buttery smooth theme toggling */ + ::view-transition-old(root), + ::view-transition-new(root) { + animation-duration: 300ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + + @media (prefers-reduced-motion: reduce) { + ::view-transition-old(root), + ::view-transition-new(root) { + animation-duration: 0ms; + } + } } diff --git a/src/components/layout/BridgeStatus.tsx b/src/components/layout/BridgeStatus.tsx new file mode 100644 index 0000000..c30bcae --- /dev/null +++ b/src/components/layout/BridgeStatus.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { onConnectionStateChange, isBridgeHealthy, restartBridge } from '@/services/bridge/bridgeClient'; +import { WifiOff, RefreshCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +const BridgeStatus = () => { + const [healthy, setHealthy] = useState(isBridgeHealthy()); + const [restarting, setRestarting] = useState(false); + + useEffect(() => { + // Sync initial state + setHealthy(isBridgeHealthy()); + + // Subscribe to changes + return onConnectionStateChange((h) => setHealthy(h)); + }, []); + + if (healthy) return null; + + const handleRestart = async () => { + if (restarting) return; + setRestarting(true); + try { + await restartBridge(); + } finally { + setRestarting(false); + } + }; + + return ( +
+
+ + Bridge Disconnected +
+ +
+ ); +}; + +export default BridgeStatus; diff --git a/src/components/layout/CommandPalette.tsx b/src/components/layout/CommandPalette.tsx index bf86693..99f97ab 100644 --- a/src/components/layout/CommandPalette.tsx +++ b/src/components/layout/CommandPalette.tsx @@ -189,7 +189,7 @@ export function CommandPalette() { const dbId = React.useMemo(() => { const parts = location.pathname.split("/"); - if (parts.length === 2 && parts[1] && !["projects", "settings"].includes(parts[1])) { + if (parts.length === 2 && parts[1] && !["settings"].includes(parts[1])) { return parts[1]; } return null; @@ -248,7 +248,6 @@ export function CommandPalette() { const pages = React.useMemo(() => [ { icon: Home, label: "Dashboard", path: "/" }, - { icon: FolderOpen, label: "Projects", path: "/projects" }, { icon: Settings, label: "Settings", path: "/settings" }, ], []); @@ -407,7 +406,7 @@ export function CommandPalette() { )} - {/* Projects */} + {/* Projects — navigate to linked database */} {projects.length > 0 && ( }> {projects.map((project) => ( @@ -418,8 +417,8 @@ export function CommandPalette() { type="project" query={search} onSelect={() => - runCommand(() => navigate("/projects"), { - id: project.id, type: "project", label: project.name, path: "/projects", + runCommand(() => navigate(`/${project.databaseId}`), { + id: project.id, type: "project", label: project.name, path: `/${project.databaseId}`, }) } /> diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx index 91289e0..3fe1d2d 100644 --- a/src/components/layout/TitleBar.tsx +++ b/src/components/layout/TitleBar.tsx @@ -1,7 +1,9 @@ -import { Minus, Square, X } from 'lucide-react'; +import { Maximize2, Minus, Square, X } from 'lucide-react'; import { getCurrentWindow } from '@tauri-apps/api/window'; +import BridgeStatus from './BridgeStatus'; const TitleBar = () => { + const handleMinimize = async () => { try { const appWindow = getCurrentWindow(); @@ -32,7 +34,7 @@ const TitleBar = () => { return (
{/* App Title - Left */}
@@ -40,29 +42,41 @@ const TitleBar = () => { RelWave
+ {/* Bridge Status - Center */} +
+ +
+ {/* Window Controls - Right */} -
+
+
+ {/* Close - Red */} + + + {/* Minimize - Yellow */} + + {/* Maximize - Green */} - +
); diff --git a/src/components/layout/VerticalIconBar.tsx b/src/components/layout/VerticalIconBar.tsx index 10aca4c..47d7a61 100644 --- a/src/components/layout/VerticalIconBar.tsx +++ b/src/components/layout/VerticalIconBar.tsx @@ -1,4 +1,4 @@ -import { Activity, Home, Database, Search, GitBranch, GitCommitHorizontal, Settings, Layers, Terminal, FolderOpen } from 'lucide-react'; +import { Activity, Home, Database, Search, GitBranch, GitCommitHorizontal, Settings, Layers, Terminal, History } from 'lucide-react'; import { Link, useLocation } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { @@ -7,7 +7,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; -export type PanelType = 'data' | 'sql-workspace' | 'query-builder' | 'schema-explorer' | 'er-diagram' | 'monitoring' | 'git-status'; +export type PanelType = 'data' | 'sql-workspace' | 'query-builder' | 'schema-explorer' | 'er-diagram' | 'monitoring' | 'git-status' | 'migrations'; interface VerticalIconBarProps { dbId?: string; @@ -18,8 +18,6 @@ interface VerticalIconBarProps { const globalNavigationItems = [ { icon: Home, label: 'Dashboard', path: '/' }, - { icon: FolderOpen, label: 'Projects', path: '/projects' }, - { icon: Settings, label: 'Settings', path: '/settings' }, ]; function supportsMonitoring(databaseType?: string) { @@ -38,13 +36,14 @@ export default function VerticalIconBar({ dbId, databaseType, activePanel, onPan }; // Database-specific panel items (only shown when dbId is provided) - const databasePanelItems: Array<{ icon: typeof Terminal; label: string; panel: PanelType }> = dbId ? [ + const databasePanelItems: Array<{ icon: any; label: string; panel: PanelType }> = dbId ? [ { icon: Layers, label: 'Data View', panel: 'data' }, { icon: Terminal, label: 'SQL Workspace', panel: 'sql-workspace' }, { icon: Search, label: 'Query Builder', panel: 'query-builder' }, { icon: GitBranch, label: 'Schema Explorer', panel: 'schema-explorer' }, { icon: Database, label: 'ER Diagram', panel: 'er-diagram' }, ...(supportsMonitoring(databaseType) ? [{ icon: Activity, label: 'Monitoring', panel: 'monitoring' as PanelType }] : []), + { icon: History, label: 'Migrations', panel: 'migrations' }, { icon: GitCommitHorizontal, label: 'Git Status', panel: 'git-status' }, ] : []; diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index 473f79e..3a3686b 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -1,6 +1,7 @@ import { createContext, useContext, useEffect, useState } from "react" +import { flushSync } from "react-dom" -type Theme = "dark" | "light" | "system" +export type Theme = "dark" | "light" | "system" type ThemeProviderProps = { children: React.ReactNode @@ -20,44 +21,78 @@ const initialState: ThemeProviderState = { const ThemeProviderContext = createContext(initialState) +function getSystemTheme(): Exclude { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" +} + +function applyTheme(theme: Theme) { + const root = window.document.documentElement + const resolvedTheme = theme === "system" ? getSystemTheme() : theme + root.classList.remove("light", "dark") + root.classList.add(resolvedTheme) +} + export function ThemeProvider({ children, defaultTheme = "system", - storageKey = "vite-ui-theme", - ...props + storageKey = "relwave:theme", }: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) + const [theme, setTheme] = useState(() => { + const stored = (localStorage.getItem(storageKey) as Theme) || defaultTheme + applyTheme(stored) + return stored + }) useEffect(() => { - const root = window.document.documentElement - - root.classList.remove("light", "dark") + applyTheme(theme) + }, [theme]) - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light" + useEffect(() => { + if (theme !== "system") return - root.classList.add(systemTheme) - return - } + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + const handleSystemThemeChange = () => applyTheme("system") - root.classList.add(theme) + mediaQuery.addEventListener("change", handleSystemThemeChange) + return () => mediaQuery.removeEventListener("change", handleSystemThemeChange) }, [theme]) const value = { theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) + setTheme: (newTheme: Theme) => { + const root = window.document.documentElement + + if (!document.startViewTransition) { + localStorage.setItem(storageKey, newTheme) + applyTheme(newTheme) + setTheme(newTheme) + return + } + + root.classList.add("theme-transitioning") + + const transition = document.startViewTransition(() => { + localStorage.setItem(storageKey, newTheme) + flushSync(() => { + applyTheme(newTheme) + setTheme(newTheme) + }) + }) + + transition.ready.catch(() => { + root.classList.remove("theme-transitioning") + }) + + transition.finished.finally(() => { + root.classList.remove("theme-transitioning") + }) }, } return ( - + {children} ) diff --git a/src/components/shared/WhatsNewDialog.tsx b/src/components/shared/WhatsNewDialog.tsx index 69654cd..c7ee451 100644 --- a/src/components/shared/WhatsNewDialog.tsx +++ b/src/components/shared/WhatsNewDialog.tsx @@ -1,155 +1,62 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { getVersion } from "@tauri-apps/api/app"; -import { Sparkles } from "lucide-react"; +import { Sparkles, ArrowRight } from "lucide-react"; +import ReactMarkdown from "react-markdown"; import { Dialog, DialogContent, - DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -type StoredInstalledUpdate = { - version: string; - body?: string; - date?: string; - previousVersion?: string; - installedAt?: string; -}; - -type NoteSection = { - title: string; - items: string[]; -}; - -/** - * Written by useUpdater after a successful in-app download. - * Contains the release body, date, previousVersion, etc. - */ -const LAST_INSTALLED_UPDATE_KEY = "relwave:last-installed-update"; +const GITHUB_REPO = "Relwave/relwave-app"; -/** - * Written on every launch with the current app version. - * Used to detect version changes across manual installs, where the - * in-app updater never runs and LAST_INSTALLED_UPDATE_KEY is never set. - */ const LAST_SEEN_VERSION_KEY = "relwave:last-seen-version"; -const TITLE_MAP: Record = { - new: "New", - added: "New", - improvements: "Improved", - improved: "Improved", - changes: "Improved", - changed: "Improved", - fixes: "Fixed", - fixed: "Fixed", - bugfixes: "Fixed", - bugs: "Fixed", - security: "Security", +type ReleaseInfo = { + version: string; + body?: string; + previousVersion?: string; }; -const FALLBACK_SECTIONS: NoteSection[] = [ - { - title: "Highlights", - items: [ - "General improvements and bug fixes.", - "Better stability and performance.", - "Quality updates to the update and database workflows.", - ], - }, -]; - -function normalizeSectionTitle(title: string): string { - const key = title.trim().toLowerCase().replace(/[^a-z]/g, ""); - return TITLE_MAP[key] ?? title.trim(); -} - -function parseReleaseBody(body?: string): NoteSection[] { - if (!body || !body.trim()) { - return FALLBACK_SECTIONS; - } - - // GitHub generates a default `notes` value when no release notes are written. - // Treat it (and similar content-free strings) as "no notes" so we fall back - // to the generic highlights instead of surfacing a useless placeholder. - const KNOWN_PLACEHOLDERS = [ - "see the assets to download this version and install", - "no release notes provided", - ]; - const normalized = body.trim().toLowerCase(); - if (KNOWN_PLACEHOLDERS.some((p) => normalized === p || normalized.startsWith(p))) { - return FALLBACK_SECTIONS; - } - - const lines = body - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean); - - if (!lines.length) { - return FALLBACK_SECTIONS; - } - - const sections: NoteSection[] = []; - let current: NoteSection = { title: "Highlights", items: [] }; - - const pushCurrentIfNeeded = () => { - if (current.items.length > 0) { - sections.push(current); - } - }; - - for (const line of lines) { - const headingMatch = line.match(/^#{1,6}\s+(.+)$/); - if (headingMatch) { - pushCurrentIfNeeded(); - current = { title: normalizeSectionTitle(headingMatch[1]), items: [] }; - continue; - } - - const bulletMatch = line.match(/^[-*]\s+(.+)$/); - if (bulletMatch) { - current.items.push(bulletMatch[1].trim()); - continue; - } - - const numberedMatch = line.match(/^\d+[.)]\s+(.+)$/); - if (numberedMatch) { - current.items.push(numberedMatch[1].trim()); - continue; - } - - // Treat plain text lines as standalone highlights. - current.items.push(line); - } - - pushCurrentIfNeeded(); - - if (!sections.length) { - return FALLBACK_SECTIONS; - } - - // Merge duplicate section titles while preserving order. - const merged: NoteSection[] = []; - for (const section of sections) { - const existing = merged.find((entry) => entry.title === section.title); - if (existing) { - existing.items.push(...section.items); - continue; - } - merged.push({ title: section.title, items: [...section.items] }); +const FALLBACK_BODY = ` +## Highlights +- General improvements and bug fixes. +- Better stability and performance. +- Quality updates to the update and database workflows. +`.trim(); + +async function fetchReleaseNotes(version: string): Promise { + const cacheKey = `relwave:release-notes-${version}`; + const cached = localStorage.getItem(cacheKey); + if (cached) return cached; + + try { + const res = await fetch( + `https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${version}`, + { + headers: { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + } + ); + if (!res.ok) return undefined; + const data = await res.json(); + const body: string | undefined = data.body ?? undefined; + if (body) localStorage.setItem(cacheKey, body); + return body; + } catch { + return undefined; } - - return merged; } export function WhatsNewDialog() { const [open, setOpen] = useState(false); - const [currentVersion, setCurrentVersion] = useState(""); - const [releaseInfo, setReleaseInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [releaseInfo, setReleaseInfo] = useState(null); useEffect(() => { let mounted = true; @@ -159,106 +66,185 @@ export function WhatsNewDialog() { const installedVersion = await getVersion(); if (!mounted) return; - setCurrentVersion(installedVersion); - const lastSeenVersion = localStorage.getItem(LAST_SEEN_VERSION_KEY); - - // Show the dialog whenever the stored version differs from the current - // version. This covers both in-app updater installs AND manual installs - // where useUpdater never runs (so LAST_INSTALLED_UPDATE_KEY is absent). - const isNewVersion = - !lastSeenVersion || lastSeenVersion !== installedVersion; - + const isNewVersion = !lastSeenVersion || lastSeenVersion !== installedVersion; if (!isNewVersion) return; - // Try to read the richer update payload written by useUpdater (contains - // the release body, date, previous version, etc.). If it's absent or - // for a different version we still show the dialog — just with fallback - // content rather than nothing at all. - let parsed: StoredInstalledUpdate | null = null; - const raw = localStorage.getItem(LAST_INSTALLED_UPDATE_KEY); - if (raw) { - try { - const candidate = JSON.parse(raw) as StoredInstalledUpdate; - // Only use the payload if it matches the installed version. - if (candidate.version === installedVersion) { - parsed = candidate; - } - } catch { - localStorage.removeItem(LAST_INSTALLED_UPDATE_KEY); - } - } - - // Fall back to a minimal record so the dialog still shows useful info. - if (!parsed) { - parsed = { - version: installedVersion, - previousVersion: lastSeenVersion ?? undefined, - }; - } + setLoading(true); + const body = await fetchReleaseNotes(installedVersion); + if (!mounted) return; - setReleaseInfo(parsed); + setReleaseInfo({ + version: installedVersion, + previousVersion: lastSeenVersion ?? undefined, + body: body ?? FALLBACK_BODY, + }); + setLoading(false); setOpen(true); } catch { - // Non-fatal: if version cannot be read, skip popup. + setLoading(false); } }; void load(); + return () => { mounted = false; }; + }, []); - return () => { - mounted = false; + // Dev-only: Cmd+Shift+W resets and reloads + useEffect(() => { + if (!import.meta.env.DEV) return; + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.shiftKey && e.key === "W") { + localStorage.removeItem(LAST_SEEN_VERSION_KEY); + Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i)) + .filter((k) => k?.startsWith("relwave:release-notes-")) + .forEach((k) => k && localStorage.removeItem(k)); + window.location.reload(); + } }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); }, []); - const sections = useMemo(() => parseReleaseBody(releaseInfo?.body), [releaseInfo?.body]); - const handleClose = (nextOpen: boolean) => { - if (!nextOpen && currentVersion) { - // Record that the user has seen the dialog for this version. - // Both keys are updated so future launches don't re-show the dialog. - localStorage.setItem(LAST_SEEN_VERSION_KEY, currentVersion); - localStorage.removeItem(LAST_INSTALLED_UPDATE_KEY); + if (!nextOpen && releaseInfo?.version) { + localStorage.setItem(LAST_SEEN_VERSION_KEY, releaseInfo.version); } setOpen(nextOpen); }; - if (!releaseInfo) { - return null; - } + if (!releaseInfo && !loading) return null; return ( - - - - - What's New in v{releaseInfo.version} - - - {releaseInfo.previousVersion - ? `Updated from v${releaseInfo.previousVersion} to v${releaseInfo.version}` - : "Thanks for updating RelWave."} - + + {/* Header */} + +
+
+ +
+
+ + What's new in RelWave + +
+ {releaseInfo?.previousVersion && ( + <> + + v{releaseInfo.previousVersion} + + + + )} + + v{releaseInfo?.version} + +
+
+
-
- {sections.map((section, index) => ( -
-

{section.title}

-
    - {section.items.map((item, itemIndex) => ( -
  • {item}
  • - ))} -
-
- ))} + {/* Body */} +
+ {loading ? ( +
+ {[80, 60, 90, 50, 70].map((w, i) => ( +
+ ))} +
+ ) : ( + ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + ul: ({ children }) => ( +
    {children}
+ ), + ol: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) => ( +
  • + + {children} +
  • + ), + p: ({ children }) => ( +

    + {children} +

    + ), + strong: ({ children }) => ( + {children} + ), + em: ({ children }) => ( + {children} + ), + hr: () => ( +
    + ), + code: ({ children, className }) => { + const isBlock = !!className; + return isBlock ? ( + + {children} + + ) : ( + + {children} + + ); + }, + a: ({ href, children }) => ( + + {children} + + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + }} + > + {releaseInfo?.body ?? ""} +
    + )}
    - - + {/* Footer */} + +

    + Thanks for keeping RelWave up to date. +

    +
    - -
    + + ); -} +} \ No newline at end of file diff --git a/src/components/ui/SQLPreviewSheet.tsx b/src/components/ui/SQLPreviewSheet.tsx new file mode 100644 index 0000000..91eb835 --- /dev/null +++ b/src/components/ui/SQLPreviewSheet.tsx @@ -0,0 +1,72 @@ +import { Check, Copy, X } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { cn } from "@/lib/utils"; + +interface SQLPreviewSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + sql: string; + title?: string; +} + +export default function SQLPreviewSheet({ + open, + onOpenChange, + sql, + title = "SQL Preview", +}: SQLPreviewSheetProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(sql); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + + + +
    +
    + + {title} + + + Preview the SQL that will be executed. + +
    +
    + + + + +
    +
    +
    + +
    +                {sql || "No SQL available."}
    +              
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..20416f0 --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { CircleIcon } from "lucide-react" +import { RadioGroup as RadioGroupPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..dcfa2b4 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as SheetPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..3ec6be7 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +export { Skeleton } diff --git a/src/features/ai/components/AIHistoryDetailDialog.tsx b/src/features/ai/components/AIHistoryDetailDialog.tsx new file mode 100644 index 0000000..f59841d --- /dev/null +++ b/src/features/ai/components/AIHistoryDetailDialog.tsx @@ -0,0 +1,274 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { + Bot, + Copy, + Check, + Trash2, + ChevronDown, + Clock, + Cpu, + Hash, + FileText, +} from "lucide-react"; +import { MarkdownRenderer } from "./AIResultDialog"; +import type { AIHistoryEntry } from "@/services/bridge/ai"; +import { cn } from "@/lib/utils"; + +interface AIHistoryDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + entry: AIHistoryEntry | null; + onDelete?: (id: number) => void; +} + +const FEATURE_LABELS: Record = { + "schema-analysis": "Schema Analysis", + "query-explanation": "Query Explanation", + "chart-recommendation": "Chart Recommendation", +}; + +const PROVIDER_LABELS: Record = { + anthropic: "Anthropic", + openai: "OpenAI", + gemini: "Gemini", + groq: "Groq", + mistral: "Mistral", + ollama: "Ollama", +}; + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "short", + }); + } catch { + return iso; + } +} + +export function AIHistoryDetailDialog({ + open, + onOpenChange, + entry, + onDelete, +}: AIHistoryDetailDialogProps) { + const [copiedField, setCopiedField] = useState<"prompt" | "response" | null>(null); + const [promptOpen, setPromptOpen] = useState(false); + + if (!entry) return null; + + const handleCopy = async (text: string, field: "prompt" | "response") => { + try { + await navigator.clipboard.writeText(text); + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + return ( + + + + +
    + +
    + Analysis Details + + {FEATURE_LABELS[entry.feature] ?? entry.feature} + +
    +
    + +
    + {/* Metadata grid */} +
    + } + label="Provider" + value={PROVIDER_LABELS[entry.provider] ?? entry.provider} + /> + } + label="Model" + value={entry.model} + /> + + + +
    + } + label="Created" + value={formatDate(entry.created_at)} + /> +
    +
    + + {entry.created_at} + +
    +
    + } + label="Tokens" + value={entry.tokens_used != null ? entry.tokens_used.toLocaleString() : "N/A"} + /> +
    + + {/* Prompt (collapsible) */} + +
    + + + + +
    + +
    +                {entry.prompt}
    +              
    +
    +
    + + {/* Response */} +
    +
    + Response + +
    +
    + +
    +
    + + {/* Delete action */} + {onDelete && ( +
    + + + + + + + Delete this analysis? + + This will permanently remove this AI analysis from your local history. + This action cannot be undone. + + + + Cancel + { + onDelete(entry.id); + onOpenChange(false); + }} + > + Delete + + + + +
    + )} +
    +
    +
    + ); +} + +function MetaItem({ + icon, + label, + value, +}: { + icon: React.ReactNode; + label: string; + value: string; +}) { + return ( +
    + {icon} +
    +

    {label}

    +

    {value}

    +
    +
    + ); +} diff --git a/src/features/ai/components/AIHistoryPanel.tsx b/src/features/ai/components/AIHistoryPanel.tsx new file mode 100644 index 0000000..fdf573a --- /dev/null +++ b/src/features/ai/components/AIHistoryPanel.tsx @@ -0,0 +1,359 @@ +import { useState, useEffect, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { History, Trash2, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; +import { aiService, type AIHistoryListItem, type AIHistoryEntry } from "@/services/bridge/ai"; +import { AIHistoryDetailDialog } from "./AIHistoryDetailDialog"; +import { cn } from "@/lib/utils"; + +const PAGE_SIZE = 10; + +const FEATURE_OPTIONS = [ + { value: "all", label: "All Features" }, + { value: "schema-analysis", label: "Schema Analysis" }, + { value: "query-explanation", label: "Query Explanation" }, + { value: "chart-recommendation", label: "Chart Recommendation" }, +]; + +const PROVIDER_OPTIONS = [ + { value: "all", label: "All Providers" }, + { value: "anthropic", label: "Anthropic" }, + { value: "openai", label: "OpenAI" }, + { value: "gemini", label: "Gemini" }, + { value: "groq", label: "Groq" }, + { value: "mistral", label: "Mistral" }, + { value: "ollama", label: "Ollama" }, +]; + +const FEATURE_LABELS: Record = { + "schema-analysis": "Schema Analysis", + "query-explanation": "Query Explanation", + "chart-recommendation": "Chart Recommendation", +}; + +const FEATURE_COLORS: Record = { + "schema-analysis": "border-violet-500/30 text-violet-600 bg-violet-500/8", + "query-explanation": "border-blue-500/30 text-blue-600 bg-blue-500/8", + "chart-recommendation": "border-amber-500/30 text-amber-600 bg-amber-500/8", +}; + +const PROVIDER_LABELS: Record = { + anthropic: "Anthropic", + openai: "OpenAI", + gemini: "Gemini", + groq: "Groq", + mistral: "Mistral", + ollama: "Ollama", +}; + +function timeAgo(isoDate: string): string { + const diff = Date.now() - new Date(isoDate).getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + const months = Math.floor(days / 30); + return `${months}mo ago`; +} + +export default function AIHistoryPanel() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [loading, setLoading] = useState(false); + const [featureFilter, setFeatureFilter] = useState("all"); + const [providerFilter, setProviderFilter] = useState("all"); + + // Detail dialog + const [detailOpen, setDetailOpen] = useState(false); + const [detailEntry, setDetailEntry] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + const fetchHistory = useCallback(async () => { + setLoading(true); + try { + const result = await aiService.getHistory({ + feature: featureFilter !== "all" ? featureFilter : undefined, + provider: providerFilter !== "all" ? providerFilter : undefined, + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }); + setItems(result.items); + setTotal(result.total); + } catch (err) { + console.error("Failed to load AI history:", err); + } finally { + setLoading(false); + } + }, [featureFilter, providerFilter, page]); + + useEffect(() => { + fetchHistory(); + }, [fetchHistory]); + + // Reset page when filters change + useEffect(() => { + setPage(0); + }, [featureFilter, providerFilter]); + + const handleRowClick = async (item: AIHistoryListItem) => { + setDetailLoading(true); + setDetailOpen(true); + try { + const entry = await aiService.getHistoryById(item.id); + setDetailEntry(entry); + } catch (err) { + console.error("Failed to load history detail:", err); + } finally { + setDetailLoading(false); + } + }; + + const handleDelete = async (id: number) => { + try { + await aiService.deleteHistory(id); + fetchHistory(); + } catch (err) { + console.error("Failed to delete history entry:", err); + } + }; + + const handleClearAll = async () => { + try { + await aiService.clearHistory(); + setItems([]); + setTotal(0); + setPage(0); + } catch (err) { + console.error("Failed to clear history:", err); + } + }; + + return ( + <> + + +
    + +
    + +
    + AI History + {total > 0 && ( + + {total} + + )} +
    + + {total > 0 && ( + + + + + + + Clear all AI history? + + This will permanently delete all {total} AI analysis entries from your local history. + Cached results will no longer be available. This action cannot be undone. + + + + Cancel + + Clear All History + + + + + )} +
    + + {/* Filters */} +
    + + +
    +
    + + + {loading ? ( +
    + +
    + ) : items.length === 0 ? ( +
    + +

    No AI analysis history yet.

    +

    + Results will appear here after you use AI features. +

    +
    + ) : ( + <> + + + + + Feature + Database + Provider + Tokens + Created + + + + {items.map((item) => ( + handleRowClick(item)} + > + + + {FEATURE_LABELS[item.feature] ?? item.feature} + + + + {item.datasource_id || ( + + )} + + + {PROVIDER_LABELS[item.provider] ?? item.provider} + + + {item.tokens_used != null ? ( + ~{item.tokens_used.toLocaleString()} + ) : ( + + )} + + + {timeAgo(item.created_at)} + + + ))} + +
    +
    + + {/* Pagination */} + {totalPages > 1 && ( +
    + + Page {page + 1} of {totalPages} · {total} entries + +
    + + +
    +
    + )} + + )} +
    +
    + + {/* Detail dialog */} + { + setDetailOpen(next); + if (!next) setDetailEntry(null); + }} + entry={detailLoading ? null : detailEntry} + onDelete={handleDelete} + /> + + ); +} diff --git a/src/features/ai/components/AIResultDialog.tsx b/src/features/ai/components/AIResultDialog.tsx new file mode 100644 index 0000000..f17f247 --- /dev/null +++ b/src/features/ai/components/AIResultDialog.tsx @@ -0,0 +1,289 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Loader2, Bot, AlertCircle, Sparkles, Copy, Check, RefreshCw } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface AIResultDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description?: string; + markdown?: string; + loading?: boolean; + error?: string | null; + /** Whether the result came from cache. */ + cached?: boolean; + /** ISO timestamp when the cached result was originally generated. */ + createdAt?: string; + /** Callback to force a fresh AI call (skip cache). */ + onReanalyze?: () => void; +} + +/** + * Format a relative time string from an ISO date. + */ +function timeAgo(isoDate: string): string { + const diff = Date.now() - new Date(isoDate).getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + const months = Math.floor(days / 30); + return `${months}mo ago`; +} + +/** + * Reusable dialog that renders AI-generated markdown output. + * Used by Schema Analysis and Query Explanation features. + */ +export function AIResultDialog({ + open, + onOpenChange, + title, + description, + markdown, + loading, + error, + cached, + createdAt, + onReanalyze, +}: AIResultDialogProps) { + return ( + + + + +
    + +
    + {title} + {/* Cached / Fresh badge */} + {markdown && !loading && !error && cached !== undefined && ( + + {cached ? "Cached" : "Fresh"} + + )} +
    + {/* Description row with optional timestamp + re-analyze */} +
    + {description && ( + {description} + )} + {markdown && !loading && cached && createdAt && ( + + Generated {timeAgo(createdAt)} + + )} + {markdown && !loading && cached && onReanalyze && ( + + )} +
    +
    + +
    + {loading ? ( +
    +
    + +
    +

    Analyzing with AI…

    +
    + ) : error ? ( +
    +
    + +
    +

    Analysis failed

    +

    + {error} +

    +

    + Make sure your AI provider is configured in{" "} + Settings → AI Settings. +

    +
    + ) : markdown ? ( + + ) : ( +
    +
    + +
    +

    No content yet.

    +
    + )} +
    +
    +
    + ); +} + +/** + * Lightweight markdown renderer — no heavy library needed. + * Handles headings, bold, code blocks, bullet lists, and paragraphs. + * + * Exported so the history detail dialog can reuse it. + */ +export function MarkdownRenderer({ content }: { content: string }) { + const lines = content.split("\n"); + const elements: React.ReactNode[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Fenced code block + if (line.trim().startsWith("```")) { + const lang = line.trim().slice(3).trim(); + const codeLines: string[] = []; + i++; + while (i < lines.length && !lines[i].trim().startsWith("```")) { + codeLines.push(lines[i]); + i++; + } + elements.push( + + ); + i++; + continue; + } + + // H1–H3 headings + const h3 = line.match(/^###\s+(.+)$/); + const h2 = line.match(/^##\s+(.+)$/); + const h1 = line.match(/^#\s+(.+)$/); + if (h1) { + elements.push(

    {renderInline(h1[1])}

    ); + i++; continue; + } + if (h2) { + elements.push(

    {renderInline(h2[1])}

    ); + i++; continue; + } + if (h3) { + elements.push(
    {renderInline(h3[1])}
    ); + i++; continue; + } + + // Bullet list items + const bullet = line.match(/^[-*]\s+(.+)$/); + if (bullet) { + const items: string[] = [bullet[1]]; + i++; + while (i < lines.length && lines[i].match(/^[-*]\s+(.+)$/)) { + items.push(lines[i].match(/^[-*]\s+(.+)$/)![1]); + i++; + } + elements.push( +
      + {items.map((item, idx) => ( +
    • + {renderInline(item)} +
    • + ))} +
    + ); + continue; + } + + // Blank line + if (!line.trim()) { + i++; + continue; + } + + // Paragraph + elements.push( +

    + {renderInline(line)} +

    + ); + i++; + } + + return
    {elements}
    ; +} + +/** Render bold (**text**) and inline code (`code`) within a text node. */ +function renderInline(text: string): React.ReactNode { + const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g); + return parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + return {part.slice(2, -2)}; + } + if (part.startsWith("`") && part.endsWith("`")) { + return {part.slice(1, -1)}; + } + return part; + }); +} + +function CodeBlock({ code, lang }: { code: string; lang: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy code: ", err); + } + }; + + return ( +
    +
    +        {code}
    +      
    + +
    + ); +} diff --git a/src/features/ai/hooks/useAISettings.ts b/src/features/ai/hooks/useAISettings.ts new file mode 100644 index 0000000..92045d3 --- /dev/null +++ b/src/features/ai/hooks/useAISettings.ts @@ -0,0 +1,22 @@ +import { loadAISettings, type AISettings } from "@/services/bridge/ai"; +import { useEffect, useState } from "react"; + +/** + * Reads AISettings from localStorage and stays in sync when + * the user updates them in the Settings page during the same session. + */ +export function useAISettings(): AISettings { + const [settings, setSettings] = useState(loadAISettings); + + useEffect(() => { + const onStorage = (e: StorageEvent) => { + if (e.key === "relwave:ai-settings") { + setSettings(loadAISettings()); + } + }; + window.addEventListener("storage", onStorage); + return () => window.removeEventListener("storage", onStorage); + }, []); + + return settings; +} diff --git a/src/features/chart/components/ChartVisualization.tsx b/src/features/chart/components/ChartVisualization.tsx index 8f1245d..f470f4f 100644 --- a/src/features/chart/components/ChartVisualization.tsx +++ b/src/features/chart/components/ChartVisualization.tsx @@ -6,6 +6,7 @@ import { ChevronDown, Sparkles, AlertCircle, + Bot, } from "lucide-react"; import { DropdownMenu, @@ -19,6 +20,10 @@ import ChartRenderer from "./ChartRenderer"; import { useChartVisualization } from "../hooks/useChartVisualization"; import { SelectedTable } from "@/features/database/types"; import { cn } from "@/lib/utils"; +import { useState } from "react"; +import { useAISettings } from "@/features/ai/hooks/useAISettings"; +import { aiService } from "@/services/bridge/ai"; +import { toast } from "sonner"; interface ChartVisualizationProps { selectedTable: SelectedTable; @@ -29,6 +34,9 @@ export const ChartVisualization = ({ selectedTable, dbId, }: ChartVisualizationProps) => { + const aiSettings = useAISettings(); + const [aiLoading, setAiLoading] = useState(false); + const { handleExport, chartType, @@ -45,6 +53,29 @@ export const ChartVisualization = ({ rowData, } = useChartVisualization(selectedTable, dbId); + const handleAISuggest = async () => { + if (!columnData.length) return; + setAiLoading(true); + try { + const rec = await aiService.recommendChart(aiSettings, { + tableName: selectedTable?.name ?? "table", + columns: columnData.map((c) => ({ + name: c.name, + type: c.type, + isPrimaryKey: c.isPrimaryKey, + })), + }); + setChartType(rec.chartType); + setXAxis(rec.xAxis); + setYAxis(rec.yAxis); + toast.success("AI suggestion applied", { description: rec.reasoning }); + } catch (err: any) { + toast.error("AI suggestion failed", { description: err?.message ?? String(err) }); + } finally { + setAiLoading(false); + } + }; + const hasData = rowData.length > 0; const isReady = !isExecuting && !errorMessage && hasData; @@ -72,6 +103,22 @@ export const ChartVisualization = ({ )} + {/* AI Suggest button */} + +
    @@ -184,15 +185,7 @@ function MigrationItem({ migration, onApply, onRollback, onDelete, onViewSQL }:

    {migration.appliedAt && (

    - Applied: {new Date(migration.appliedAt).toLocaleString('en-IN', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: true, - timeZone: 'Asia/Kolkata' - })} + Applied: {formatTimestamp(migration.appliedAt)}

    )}
    diff --git a/src/features/git/components/GitStatusPanel.tsx b/src/features/git/components/GitStatusPanel.tsx index 3a1df09..8108c12 100644 --- a/src/features/git/components/GitStatusPanel.tsx +++ b/src/features/git/components/GitStatusPanel.tsx @@ -14,6 +14,8 @@ import { Eye, RotateCcw, User, + RefreshCw, + UploadCloud, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,6 +43,7 @@ import { Spinner } from "@/components/ui/spinner"; import { toast } from "sonner"; import type { GitFileChange, GitLogEntry } from "@/features/git/types"; import { gitService } from "@/services/bridge/git"; +import { projectService } from "@/services/bridge/project"; import { GitHistoryGraph } from "./GitHistoryGraph"; // ─── Helpers ────────────────────────────────────────── @@ -100,11 +103,12 @@ function timeAgo(dateStr: string): string { interface GitStatusPanelProps { projectDir: string | null | undefined; + projectId?: string; } -export default function GitStatusPanel({ projectDir }: GitStatusPanelProps) { - const { data: status, isLoading: statusLoading } = useGitStatus(projectDir); - const { data: changes } = useGitChanges( +export default function GitStatusPanel({ projectDir, projectId }: GitStatusPanelProps) { + const { data: status, isLoading: statusLoading, refetch: refetchStatus } = useGitStatus(projectDir); + const { data: changes, refetch: refetchChanges } = useGitChanges( status?.isGitRepo ? projectDir : undefined ); const { data: logGraph } = useGitLogGraph( @@ -121,6 +125,9 @@ export default function GitStatusPanel({ projectDir }: GitStatusPanelProps) { const [diffFile, setDiffFile] = useState(""); const [diffLoading, setDiffLoading] = useState(false); + const [syncing, setSyncing] = useState(false); + const [pushing, setPushing] = useState(false); + if (!projectDir) { return (
    @@ -176,11 +183,41 @@ export default function GitStatusPanel({ projectDir }: GitStatusPanelProps) { ); }; + const handleSync = async () => { + if (!projectId) return; + setSyncing(true); + try { + await projectService.syncMigrations(projectId); + toast.success("Migrations synced to git"); + refetchStatus(); + refetchChanges(); + } catch (err: any) { + toast.error(`Failed to sync migrations: ${err.message}`); + } finally { + setSyncing(false); + } + }; + + const handlePush = async () => { + if (!projectId) return; + setPushing(true); + try { + await projectService.pushMigrations(projectId); + toast.success("Migrations pushed to remote"); + refetchStatus(); + refetchChanges(); + } catch (err: any) { + toast.error(`Failed to push migrations: ${err.message}`); + } finally { + setPushing(false); + } + }; + return (
    {/* Header */}
    -
    +

    Git Status

    @@ -211,6 +248,23 @@ export default function GitStatusPanel({ projectDir }: GitStatusPanelProps) { )}
    + {projectId && ( +
    + + Migration Sync + +
    + + +
    +
    + )}
    {/* Tabs */} @@ -354,37 +408,41 @@ export default function GitStatusPanel({ projectDir }: GitStatusPanelProps) { {/* Diff Dialog */} - + {diffFile} - +
    {diffLoading ? ( -
    +
    ) : ( -
    -                                {diffContent.split("\n").map((line, i) => {
    -                                    let color = "text-foreground/80";
    -                                    if (line.startsWith("+") && !line.startsWith("+++"))
    -                                        color = "text-green-500";
    -                                    else if (line.startsWith("-") && !line.startsWith("---"))
    -                                        color = "text-red-500";
    -                                    else if (line.startsWith("@@"))
    -                                        color = "text-blue-400";
    -                                    return (
    -                                        
    - {line} -
    - ); - })} -
    + +
    +                                    {diffContent.split("\n").map((line, i) => {
    +                                        let color = "text-foreground/80";
    +
    +                                        if (line.startsWith("+") && !line.startsWith("+++"))
    +                                            color = "text-green-500";
    +                                        else if (line.startsWith("-") && !line.startsWith("---"))
    +                                            color = "text-red-500";
    +                                        else if (line.startsWith("@@"))
    +                                            color = "text-blue-400";
    +
    +                                        return (
    +                                            
    + {line} +
    + ); + })} +
    +
    )} - +
    diff --git a/src/features/home/components/ConnectionDetails.tsx b/src/features/home/components/ConnectionDetails.tsx index 338a7ec..d1fddf0 100644 --- a/src/features/home/components/ConnectionDetails.tsx +++ b/src/features/home/components/ConnectionDetails.tsx @@ -1,5 +1,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { DatabaseConnection } from "@/features/database/types" +import { formatTimestamp } from "@/lib/utils" export function ConnectionDetails({ database }: { database: DatabaseConnection }) { return ( @@ -41,7 +42,7 @@ export function ConnectionDetails({ database }: { database: DatabaseConnection }
    Created - {new Date(database.createdAt).toLocaleDateString()} + {formatTimestamp(database.createdAt)}
    diff --git a/src/features/home/components/ConnectionList.tsx b/src/features/home/components/ConnectionList.tsx index 0b77b85..52c2acb 100644 --- a/src/features/home/components/ConnectionList.tsx +++ b/src/features/home/components/ConnectionList.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from "react"; -import { Plus, Database, Search, Trash2, Zap, Folder, ChevronRight, ChevronDown, MoreVertical, Edit2, FolderPlus, GripVertical, FolderMinus, Shield } from "lucide-react"; +import { Plus, Database, Search, Trash2, Zap, Folder, ChevronRight, ChevronDown, MoreVertical, Edit2, FolderPlus, GripVertical, FolderMinus, Shield, FolderInput, X, Settings as SettingsIcon, CircleDot } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { @@ -48,6 +48,8 @@ import { import { CSS } from "@dnd-kit/utilities"; import { DatabaseConnection } from "@/features/database/types"; import { InputDialog } from "@/components/shared/InputDialog"; +import { SettingsDialog } from "@/features/settings/components"; +import { UnlinkedProjectItem } from "@/features/project/components/UnlinkedProjectItem"; // --- Components --- @@ -134,7 +136,7 @@ function DraggableConnectionItem({
    @@ -286,22 +288,26 @@ function DroppableGroup({ // --- Main Component --- -export function ConnectionList({ - databases, - filteredDatabases, - loading, - searchQuery, - setSearchQuery, - selectedDb, - setSelectedDb, - status, - connectedCount, - totalTables, +export function ConnectionList({ + databases, + filteredDatabases, + unlinkedProjects = [], + loading, + searchQuery, + setSearchQuery, + onlineFilter, + setOnlineFilter, + selectedDb, + setSelectedDb, + status, + connectedCount, + totalTables, statsLoading, onAddClick, onDatabaseHover, onDelete, onTest, + onImportClick }: ConnectionListProps) { const { groups, @@ -320,6 +326,7 @@ export function ConnectionList({ const [newGroupOpen, setNewGroupOpen] = useState(false); const [renameGroupOpen, setRenameGroupOpen] = useState(false); const [groupToRename, setGroupToRename] = useState(null); + const [settingsOpen, setSettingsOpen] = useState(false); const sensors = useSensors( useSensor(PointerSensor, { @@ -377,6 +384,18 @@ export function ConnectionList({

    New Group

    + {onImportClick && ( + + + + + +

    Import Project

    +
    +
    + )}
    + + {onlineFilter && ( +
    +
    + + Filtered: Online only + +
    +
    + )} {/* Database List */}
    @@ -446,11 +481,13 @@ export function ConnectionList({ "mt-2 pt-2 border-t border-border/20 rounded-lg transition-colors border border-transparent min-h-20 pb-4", isOverUngrouped ? "bg-primary/5 border-primary/20 shadow-inner" : "" )}> -
    - - Ungrouped - -
    + {groups.length > 0 && ( +
    + + ungrouped + +
    + )}
    c.id)} strategy={verticalListSortingStrategy}> {ungroupedConnections.map(db => ( @@ -470,6 +507,25 @@ export function ConnectionList({
    + {/* Unlinked Projects Section */} + {unlinkedProjects && unlinkedProjects.length > 0 && ( +
    +
    + + unlinked projects + +
    +
    + {unlinkedProjects.map((project) => ( + + ))} +
    +
    + )} +
    {filteredDatabasesById.get(activeId)?.name}
    @@ -499,7 +555,7 @@ export function ConnectionList({ {/* Quick Stats Footer */}
    -
    +
    @@ -517,6 +573,11 @@ export function ConnectionList({
    +
    + +
    {/* Dialogs */} @@ -540,6 +601,8 @@ export function ConnectionList({ onConfirm={(name) => renameGroup(groupToRename.id, name)} /> )} + +
    ); } diff --git a/src/features/home/components/DatabaseDetail.tsx b/src/features/home/components/DatabaseDetail.tsx index 0978050..5b3d699 100644 --- a/src/features/home/components/DatabaseDetail.tsx +++ b/src/features/home/components/DatabaseDetail.tsx @@ -9,17 +9,22 @@ import { Trash2, CircleDot, ArrowLeft, + Download, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { DatabaseDetailProps } from "../types"; import { DatabaseOverviewPanel } from "./DatabaseOverviewPanel"; +import { useProjectByDatabaseId, useProjectSchema, useProjectERDiagram, useProjectQueries } from "@/features/project/hooks/useProjectQueries"; +import { projectService } from "@/services/bridge/project"; +import { toast } from "sonner"; const DB_COLORS: Record = { postgresql: { bg: "bg-blue-500/10", text: "text-blue-500" }, @@ -42,6 +47,35 @@ export function DatabaseDetail({ size, tables }: DatabaseDetailProps) { + // Fetch linked project and its sub-resources + const { data: project } = useProjectByDatabaseId(database.id); + const { data: schemaData } = useProjectSchema(project?.id); + const { data: erData } = useProjectERDiagram(project?.id); + const { data: queriesData } = useProjectQueries(project?.id); + + const handleExport = async () => { + if (!project?.id) return; + try { + const bundle = await projectService.exportProject(project.id); + if (!bundle) { + toast.error("Project not found"); + return; + } + const blob = new Blob([JSON.stringify(bundle, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${bundle.metadata.name.replace(/\s+/g, "-").toLowerCase()}-export.json`; + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 0); + toast.success("Project exported"); + } catch (err: any) { + toast.error("Export failed", { description: err.message }); + } + }; + return (
    {/* Detail Header */} @@ -115,6 +149,15 @@ export function DatabaseDetail({ + {project && ( + <> + + + Export Project + + + + )} 0} + queryCount={queriesData?.queries?.length} />
    diff --git a/src/features/home/components/DatabaseOverviewPanel.tsx b/src/features/home/components/DatabaseOverviewPanel.tsx index e317feb..7ede4e4 100644 --- a/src/features/home/components/DatabaseOverviewPanel.tsx +++ b/src/features/home/components/DatabaseOverviewPanel.tsx @@ -1,21 +1,30 @@ -import { Card, CardAction, CardContent, CardHeader } from "@/components/ui/card"; -import { Clock, Database, HardDrive, Table2 } from "lucide-react"; -import { formatRelativeTime } from "../utils"; +import { Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Clock, Database, HardDrive, Table2, Layers, GitBranch, FileCode2 } from "lucide-react"; +import { formatRelativeTime } from "@/lib/utils"; import { DatabaseConnection } from "@/features/database/types"; import { ConnectionDetails } from "./ConnectionDetails"; - +import { MigrationStatusCard } from "./MigrationStatusCard"; interface DatabaseOverviewPanelProps { database: DatabaseConnection; + projectId?: string; tables: number | string | undefined; size: string | number | undefined; + // Project data props + schemaCount?: number; + hasERLayout?: boolean; + queryCount?: number; } export function DatabaseOverviewPanel({ database, + projectId, size, - tables + tables, + schemaCount, + hasERLayout, + queryCount, }: DatabaseOverviewPanelProps) { return ( @@ -69,6 +78,102 @@ export function DatabaseOverviewPanel({
    + {/* Project Data */} + {projectId && ( + <> +

    Project Data

    +
    + + + + + + + + ER Diagram + + + {hasERLayout ? "Saved" : "—"} + + + Diagram layout + + + + + + + + + Saved Queries + + + {queryCount ?? "—"} + + + Stored queries + + +
    + + )} + + {!projectId && ( + <> +

    Project Data

    +
    + + + + + + Schema Cache + + + {schemaCount ?? "—"} + + + Cached schemas + + + + + + + + + ER Diagram + + + {hasERLayout ? "Saved" : "—"} + + + Diagram layout + + + + + + + + + Saved Queries + + + {queryCount ?? "—"} + + + Stored queries + + +
    + + )} + {/* Connection Details */} diff --git a/src/features/home/components/DeleteConnectionDialog.tsx b/src/features/home/components/DeleteConnectionDialog.tsx new file mode 100644 index 0000000..5dbdd16 --- /dev/null +++ b/src/features/home/components/DeleteConnectionDialog.tsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; + +type DeleteConnectionDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + connectionName: string; + projectName: string; + hasGitRemote: boolean; + gitRemoteUrl?: string; + onConfirm: (choice: "unlink" | "delete_project") => Promise; +}; + +export function DeleteConnectionDialog({ + open, + onOpenChange, + connectionName, + projectName, + hasGitRemote, + gitRemoteUrl, + onConfirm, +}: DeleteConnectionDialogProps) { + const [step, setStep] = useState<1 | 2>(1); + const [choice, setChoice] = useState<"unlink" | "delete_project">("unlink"); + const [isDeleting, setIsDeleting] = useState(false); + + // Reset state when dialog opens + useEffect(() => { + if (open) { + setStep(1); + setChoice("unlink"); + setIsDeleting(false); + } + }, [open]); + + const handleNext = async () => { + if (choice === "unlink") { + // Execute unlink flow directly, no second step + setIsDeleting(true); + try { + await onConfirm("unlink"); + } finally { + setIsDeleting(false); + } + } else { + // Proceed to step 2 for project deletion confirmation + setStep(2); + } + }; + + const handleConfirmDelete = async () => { + setIsDeleting(true); + try { + await onConfirm("delete_project"); + } finally { + setIsDeleting(false); + } + }; + + return ( + + + + {step === 1 ? ( + <> + Delete Connection + + {connectionName} + + + ) : ( + + Are you absolutely sure? + + )} + + +
    + {step === 1 ? ( +
    +

    + This connection is linked to the project{" "} + "{projectName}". +

    +

    What would you like to do with the project?

    + setChoice(val)} + className="space-y-3 mt-4" + > +
    setChoice("unlink")}> + + +
    +
    setChoice("delete_project")}> + + +
    +
    +
    + ) : ( +
    +

    + This will permanently delete: +

    +
      +
    • migrations/
    • +
    • schema.json
    • +
    • diagrams/
    • +
    • All saved queries
    • +
    +

    + This cannot be undone. +

    + + {hasGitRemote && gitRemoteUrl && ( +
    + + ⚠ Note + + + The remote repository at {gitRemoteUrl} will not be deleted. Only the local project folder will be removed. + +
    + )} +
    + )} +
    + + + {step === 1 ? ( + <> + + + + ) : ( + <> + + + + )} + +
    +
    + ); +} diff --git a/src/features/home/components/DiscoveredDatabasesCard.tsx b/src/features/home/components/DiscoveredDatabasesCard.tsx index be8a081..e912f59 100644 --- a/src/features/home/components/DiscoveredDatabasesCard.tsx +++ b/src/features/home/components/DiscoveredDatabasesCard.tsx @@ -3,11 +3,12 @@ import { Radar, Plus, Container, Monitor, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useDiscoveredDatabases } from "@/features/database/hooks/useDiscoveredDatabases"; -import { DiscoveredDatabase } from "@/features/database/types"; +import { DatabaseConnection, DiscoveredDatabase } from "@/features/database/types"; import { Card, CardAction, CardContent, CardDescription, CardTitle } from "@/components/ui/card"; interface DiscoveredDatabasesCardProps { onAddDatabase: (db: DiscoveredDatabase) => void; + existingConnections: DatabaseConnection[]; } const DB_TYPE_COLORS = { @@ -35,6 +36,7 @@ const DB_TYPE_COLORS = { export function DiscoveredDatabasesCard({ onAddDatabase, + existingConnections, }: DiscoveredDatabasesCardProps) { const { databases, isScanning, scan, lastScanned } = useDiscoveredDatabases(); @@ -86,8 +88,9 @@ export function DiscoveredDatabasesCard({ return ( @@ -107,7 +110,7 @@ export function DiscoveredDatabasesCard({ {/* Details */} - +
    {db.containerName && ( - + Container: {db.containerName} )} - - - Suggested: {db.suggestedName} - + +
    + + Suggested: {db.suggestedName} + + + {existingConnections.some(c => c.host === db.host && String(c.port) === String(db.port)) ? ( + Already added + ) : ( + + )} +
    - - {/* Add Button */} -
    ); diff --git a/src/features/home/components/MigrationStatusCard.tsx b/src/features/home/components/MigrationStatusCard.tsx new file mode 100644 index 0000000..a9e3a6d --- /dev/null +++ b/src/features/home/components/MigrationStatusCard.tsx @@ -0,0 +1,110 @@ +import { useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useImportAnalysis } from "@/features/project/hooks/useImportAnalysis"; +import { AlertTriangle, Database, RefreshCw, CheckCircle2 } from "lucide-react"; +import { SchemaDriftSheet } from "@/features/project/components/SchemaDriftSheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; + +interface MigrationStatusCardProps { + projectId: string; + databaseId: string; + connectionName: string; +} + +export function MigrationStatusCard({ projectId, databaseId, connectionName }: MigrationStatusCardProps) { + const { analysis, loading, refetch } = useImportAnalysis(projectId, databaseId); + const [driftSheetOpen, setDriftSheetOpen] = useState(false); + + if (loading && !analysis) { + return ; + } + + if (!analysis) return null; + + const isDrifted = analysis.driftStatus === "drifted"; + const isSynced = analysis.driftStatus === "synced"; + + return ( + <> + + + +
    + + Migration Status +
    + {isSynced ? ( + + + Synced + + ) : isDrifted ? ( + + + Drift Detected + + ) : ( + + {Math.max(0, analysis.migrationCount)} Pending + + )} +
    + + State of the live database against the project schema. + +
    + +
    +
    + Pending Migrations + {Math.max(0, analysis.migrationCount)} +
    + + {isDrifted && analysis.driftDetails && ( +
    + Schema drift detected. The live database has modifications not tracked in the schema snapshot. +
    + )} + + {isDrifted && !analysis.driftDetails && ( +
    + Drift detected — pending migrations have not been applied to the live database. +
    + )} + +
    + + {isDrifted && ( + + )} +
    +
    +
    +
    + + {/* Always render the sheet — it manages its own empty-state guard */} + + + ); +} diff --git a/src/features/home/components/WelcomeView.tsx b/src/features/home/components/WelcomeView.tsx index cb97e87..e18aa57 100644 --- a/src/features/home/components/WelcomeView.tsx +++ b/src/features/home/components/WelcomeView.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Plus, Database, @@ -9,12 +10,18 @@ import { Layers, } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { cn, formatRelativeTime } from "@/lib/utils"; import { WelcomeViewProps } from "../types"; -import { formatRelativeTime } from "../utils"; import { DiscoveredDatabasesCard } from "./DiscoveredDatabasesCard"; import { Spinner } from "@/components/ui/spinner"; import { Card, CardAction, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useDbStats, useTables } from "@/features/project/hooks/useDbQueries"; +import { bytesToMBString } from "@/lib/bytesToMB"; +import { DatabaseConnection } from "@/features/database/types"; +import { useCountUp } from "@/hooks/useCountUp"; const DB_COLORS: Record = { @@ -28,6 +35,44 @@ function getDbColors(type: string) { return DB_COLORS[type] || { bg: "bg-primary/10", text: "text-primary" }; } +function ConnectionSizeItem({ db }: { db: DatabaseConnection }) { + const { data: stats, isLoading } = useDbStats(db.id); + const sizeStr = stats?.sizeBytes ? bytesToMBString(stats.sizeBytes) : "—"; + return ( +
    +
    + + {db.name} +
    + {isLoading ? "..." : sizeStr} +
    + ); +} + +function ConnectionTablesList({ db }: { db: DatabaseConnection }) { + const { data: tables, isLoading } = useTables(db.id); + if (isLoading) return
    Loading tables for {db.name}...
    ; + if (!tables || tables.length === 0) return null; + return ( +
    +
    +
    + +
    +

    {db.name}

    +
    +
    + {tables.map(t => ( +
    + + {t.schema !== 'public' && t.schema !== db.database ? `${t.schema}.` : ''}{t.name} +
    + ))} +
    +
    + ); +} + export function WelcomeView({ databases, recentDatabases, @@ -36,40 +81,66 @@ export function WelcomeView({ totalTables, totalSize, statsLoading, - welcomeMessage, onAddClick, onSelectDb, onDatabaseHover, onDiscoveredDatabaseAdd, + onOnlineFilterClick, }: WelcomeViewProps) { + const [showAllActivity, setShowAllActivity] = useState(false); + + const hour = new Date().getHours(); + let timeGreeting = "Good morning"; + if (hour >= 12 && hour < 17) timeGreeting = "Good afternoon"; + else if (hour >= 17) timeGreeting = "Good evening"; + + const lastActiveName = recentDatabases[0]?.name; + + const animatedConnections = useCountUp(databases.length); + const animatedOnline = useCountUp(connectedCount); + const animatedTables = useCountUp(typeof totalTables === 'number' ? totalTables : 0); return ( -
    - {/* Welcome Header */} -
    -
    -
    -
    - -
    -

    - {welcomeMessage} -

    -
    -

    - Select a connection or add a new one -

    -
    +
    + {/* Welcome Header (Subtle Greeting) */} +
    + {databases.length === 0 ? ( + <> + {timeGreeting} + + + + ) : ( + <> + {timeGreeting} + + {connectedCount} connections online + {lastActiveName && ( + <> + + Last active: {lastActiveName} + + )} + + )}
    {/* Stats Overview */}
    - + document.getElementById('connection-search')?.focus()} + >
    Total Connections - {databases.length} + {animatedConnections}
    @@ -78,12 +149,15 @@ export function WelcomeView({
    - +
    Online Now - {connectedCount} + {animatedOnline}
    @@ -92,109 +166,169 @@ export function WelcomeView({
    - - -
    - Total Tables - - {statsLoading ? ( -
    - -
    - ) : ( - totalTables - )} -
    -
    - - - -
    -
    + + + + +
    + Total Tables + + {statsLoading ? ( +
    + +
    + ) : ( + typeof totalTables === 'number' ? animatedTables : totalTables + )} +
    +
    + + + +
    +
    +
    + + + All Tables + + + {databases.length === 0 ? ( +
    No databases connected.
    + ) : ( + databases.map(db => ) + )} +
    +
    +
    - - -
    - Data Size - - {statsLoading ? ( -
    - -
    - ) : ( - totalSize - )} -
    -
    - - - -
    -
    + + + + +
    + Data Size + + {statsLoading ? ( +
    + +
    + ) : ( + totalSize + )} +
    +
    + + + +
    +
    +
    + + {databases.length === 0 ? ( +
    No databases connected.
    + ) : ( + databases.map(db => ) + )} +
    +
    {/* Discovered Databases */} { onDiscoveredDatabaseAdd && ( - + ) } {/* Recent Activity */} { - recentDatabases.length > 0 && ( -
    -
    - -

    - Recent Activity -

    -
    -
    - {recentDatabases.map((db, index) => { - const isConnected = status.get(db.id) === "connected"; - return ( -
    -
    -

    {db.name}

    -

    - {formatRelativeTime(db.lastAccessedAt)} • {db.type} -

    -
    - + > + +
    +
    +

    {db.name}

    +

    + {db.type} • {db.host}:{db.port} +

    +
    +
    + {formatRelativeTime(db.lastAccessedAt)} +
    + + ); + })} + + {/* Empty Padding Slots */} + {Array.from({ length: emptySlots }).map((_, i) => ( +
    5) && "border-b border-border/30" + )} + /> + ))} + + {/* View All Toggle */} + {recentDatabases.length > 5 && ( + - ); - })} + )} +
    -
    - ) + ); + })() } {/* Empty State */} diff --git a/src/features/home/components/index.ts b/src/features/home/components/index.ts index d183bf5..765f30b 100644 --- a/src/features/home/components/index.ts +++ b/src/features/home/components/index.ts @@ -3,6 +3,6 @@ export { DatabaseDetail } from "./DatabaseDetail"; export { WelcomeView } from "../components/WelcomeView"; export { AddConnectionDialog } from "./AddConnectionDialog"; export { DeleteDialog } from "./DeleteDialog"; +export { DeleteConnectionDialog } from "./DeleteConnectionDialog"; export { DiscoveredDatabasesCard } from "./DiscoveredDatabasesCard"; export * from "../types"; -export * from "../utils"; diff --git a/src/features/home/hooks/useDeleteConnection.ts b/src/features/home/hooks/useDeleteConnection.ts new file mode 100644 index 0000000..c5f93c5 --- /dev/null +++ b/src/features/home/hooks/useDeleteConnection.ts @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { toast } from "sonner"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { databaseService } from "@/services/bridge/database"; +import { bridgeRequest } from "@/services/bridge/bridgeClient"; +import { projectKeys } from "@/features/project/hooks/useProjectQueries"; + +type DeleteConnectionDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + connectionName: string; + projectName: string; + projectPath: string; + hasGitRemote: boolean; + gitRemoteUrl?: string; + onConfirm: (choice: "unlink" | "delete_project") => Promise; +}; + +export function useDeleteConnection(onSuccess?: () => void) { + const queryClient = useQueryClient(); + + // Core state + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogProps, setDialogProps] = useState | null>(null); + const [isDeleting, setIsDeleting] = useState(false); + + // Database deletion mutation (simple path or final step of complex path) + const deleteDatabaseMutation = useMutation({ + mutationFn: databaseService.deleteDatabase, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["databases"] }); + onSuccess?.(); + }, + }); + + const initiateDelete = async (databaseId: string, databaseName: string) => { + setIsDeleting(true); + try { + // 1. Check for linked project + const res = await bridgeRequest("project.getByDatabaseId", { databaseId }); + const linkedProject = res?.data; + + if (!linkedProject) { + // 2. No project -> call existing delete directly + await deleteDatabaseMutation.mutateAsync(databaseId); + toast.success("Database removed"); + } else { + // 3. Project exists -> fetch git remote, open dialog + const projectPathRes = await bridgeRequest("project.getDir", { projectId: linkedProject.id }); + const projectPath = projectPathRes?.data?.dir || ""; + + const gitRes = await bridgeRequest("project.getGitRemote", { projectPath }); + const remoteUrl = gitRes?.data?.remoteUrl; + + setDialogProps({ + connectionName: databaseName, + projectName: linkedProject.name, + projectPath, + hasGitRemote: !!remoteUrl, + gitRemoteUrl: remoteUrl || undefined, + onConfirm: async (choice) => { + try { + if (choice === "unlink") { + await bridgeRequest("project.unlinkFromConnection", { databaseId }); + } else { + await bridgeRequest("project.deleteWithConnection", { databaseId }); + // Invalidate projects since one was just deleted + queryClient.invalidateQueries({ queryKey: projectKeys.all }); + } + + // Delete the DB connection itself + await deleteDatabaseMutation.mutateAsync(databaseId); + toast.success("Database removed"); + setDialogOpen(false); + } catch (err: any) { + toast.error("Failed to delete", { description: err.message }); + throw err; // throw to keep dialog open if error happens + } + } + }); + + setDialogOpen(true); + } + } catch (err: any) { + toast.error("Failed to initiate delete", { description: err.message }); + } finally { + setIsDeleting(false); + } + }; + + return { + initiateDelete, + dialogOpen, + setDialogOpen, + dialogProps, + isDeleting: deleteDatabaseMutation.isPending || isDeleting, + }; +} diff --git a/src/features/home/hooks/useIndexPage.ts b/src/features/home/hooks/useIndexPage.ts index c237e0e..ac514a7 100644 --- a/src/features/home/hooks/useIndexPage.ts +++ b/src/features/home/hooks/useIndexPage.ts @@ -3,17 +3,22 @@ import { useState, useCallback, useMemo, useEffect } from "react"; import { toast } from "sonner"; import { useNavigate, useLocation } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; import { useDatabases, useAddDatabase, useDeleteDatabase, usePrefetch } from "@/features/project/hooks/useDbQueries"; +import { projectKeys, useProjects } from "@/features/project/hooks/useProjectQueries"; import { ConnectionFormData, REQUIRED_FIELDS, SQLITE_REQUIRED_FIELDS } from "@/features/home/types"; import { useDatabaseStats } from "../../database/hooks/useDatabaseStats"; import { useSelectedDbStats } from "../../database/hooks/useSelectedDbStats"; import { databaseService } from "@/services/bridge/database"; +import { projectService } from "@/services/bridge/project"; +import { useDeleteConnection } from "./useDeleteConnection"; import { DatabaseConnection } from "@/features/database/types"; import { useWelcomeMessage } from "@/features/database/hooks/useWelcomeMessage"; export const useIndexPage = (bridgeReady: boolean) => { const navigate = useNavigate(); const location = useLocation(); + const queryClient = useQueryClient(); // ... existing logic ... @@ -38,20 +43,18 @@ export const useIndexPage = (bridgeReady: boolean) => { refetchStatus, } = useDatabaseStats(bridgeReady, databases.length > 0); - const welcomeMessage = useWelcomeMessage(); // Mutations const addDatabaseMutation = useAddDatabase(); - const deleteDatabaseMutation = useDeleteDatabase(); const { prefetchTables, prefetchStats } = usePrefetch(); // UI state const [searchQuery, setSearchQuery] = useState(""); + const [onlineFilter, setOnlineFilter] = useState(false); const [selectedDb, setSelectedDb] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [dbToDelete, setDbToDelete] = useState<{ id: string; name: string } | null>(null); const [prefilledConnectionData, setPrefilledConnectionData] = useState | undefined>(undefined); + const [isImportOpen, setIsImportOpen] = useState(false); // Selected db derived state const selectedDatabase = useMemo( @@ -67,22 +70,33 @@ export const useIndexPage = (bridgeReady: boolean) => { const filteredDatabases = useMemo( () => databases.filter( - (db: DatabaseConnection) => - db.name.toLowerCase().includes(searchQuery.toLowerCase()) || - db.host.toLowerCase().includes(searchQuery.toLowerCase()) + (db: DatabaseConnection) => { + const matchesSearch = db.name.toLowerCase().includes(searchQuery.toLowerCase()) || + db.host.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesOnline = onlineFilter ? status.get(db.id) === "connected" : true; + return matchesSearch && matchesOnline; + } ), - [databases, searchQuery] + [databases, searchQuery, onlineFilter, status] ); const recentDatabases = useMemo( () => [...databases] .filter((db) => db.lastAccessedAt) - .sort((a, b) => new Date(b.lastAccessedAt!).getTime() - new Date(a.lastAccessedAt!).getTime()) - .slice(0, 5), + .sort((a, b) => new Date(b.lastAccessedAt!).getTime() - new Date(a.lastAccessedAt!).getTime()), [databases] ); + // Projects list + const { data: projects = [] } = useProjects(); + + // Unlinked projects + const unlinkedProjects = useMemo( + () => projects.filter((p: any) => p.status === "unlinked" || !p.databaseId), + [projects] + ); + // ---- Bridge Handlers ---- const handleAddDatabase = async (formData: ConnectionFormData) => { @@ -145,7 +159,21 @@ export const useIndexPage = (bridgeReady: boolean) => { }; } - await addDatabaseMutation.mutateAsync(payload); + const db = await addDatabaseMutation.mutateAsync(payload); + + // Auto-create a linked project so the user gets git, schema cache, + // and saved queries for free — one click, two things. + try { + await projectService.createProject({ + databaseId: db.id, + name: db.name, + defaultSchema: db.type === "postgresql" ? "public" : undefined, + }); + } catch { + // Non-fatal — project auto-creation shouldn't block the database add + console.warn("Auto-project creation failed for", db.name); + } + toast.success("Database connection added"); setIsDialogOpen(false); await Promise.all([refetchDatabases(), refetchStatus()]); @@ -154,20 +182,6 @@ export const useIndexPage = (bridgeReady: boolean) => { } }; - const handleDeleteDatabase = async () => { - if (!dbToDelete) return; - try { - await deleteDatabaseMutation.mutateAsync(dbToDelete.id); - toast.success("Database removed"); - setDeleteDialogOpen(false); - setDbToDelete(null); - if (selectedDb === dbToDelete.id) setSelectedDb(null); - refetchDatabases(); - } catch (err: any) { - toast.error("Failed to delete", { description: err.message }); - } - }; - const handleTestConnection = async (id: string, name: string) => { try { const result = await databaseService.testConnection(id); @@ -196,11 +210,6 @@ export const useIndexPage = (bridgeReady: boolean) => { // ---- Dialog Helpers ---- - const openDeleteDialog = (id: string, name: string) => { - setDbToDelete({ id, name }); - setDeleteDialogOpen(true); - }; - const handleDiscoveredDatabaseAdd = useCallback( (db: { type: string; @@ -232,15 +241,37 @@ export const useIndexPage = (bridgeReady: boolean) => { if (!open) setPrefilledConnectionData(undefined); }; + // ---- Delete Hook ---- + const { + initiateDelete, + dialogOpen: deleteDialogOpen, + setDialogOpen: setDeleteDialogOpen, + dialogProps: deleteConnectionDialogProps, + isDeleting + } = useDeleteConnection(() => { + if (selectedDb) setSelectedDb(null); + refetchDatabases(); + }); + + const openDeleteDialog = (id: string, name: string) => { + initiateDelete(id, name); + }; + + const handleImportComplete = async (_projectId: string, _projectName: string) => { + setIsImportOpen(false); + queryClient.invalidateQueries({ queryKey: projectKeys.all }); + await Promise.all([refetchDatabases(), refetchStatus()]); + }; + return { // Data databases, filteredDatabases, recentDatabases, + unlinkedProjects, selectedDatabase, selectedDbStats, loading, - welcomeMessage, // Status + stats status, @@ -256,23 +287,30 @@ export const useIndexPage = (bridgeReady: boolean) => { // UI state searchQuery, setSearchQuery, + onlineFilter, + setOnlineFilter, selectedDb, setSelectedDb, isDialogOpen, setIsDialogOpen, deleteDialogOpen, setDeleteDialogOpen, - dbToDelete, + deleteConnectionDialogProps, + isDeleting, prefilledConnectionData, // Handlers handleAddDatabase, - handleDeleteDatabase, handleTestConnection, handleDatabaseClick, handleDatabaseHover, handleDiscoveredDatabaseAdd, handleDialogClose, openDeleteDialog, + + // Import + isImportOpen, + setIsImportOpen, + handleImportComplete, }; }; \ No newline at end of file diff --git a/src/features/home/types.ts b/src/features/home/types.ts index 6543769..f197b97 100644 --- a/src/features/home/types.ts +++ b/src/features/home/types.ts @@ -3,9 +3,12 @@ import { DatabaseConnection, DiscoveredDatabase } from "@/features/database/type export interface ConnectionListProps { databases: DatabaseConnection[]; filteredDatabases: DatabaseConnection[]; + unlinkedProjects?: any[]; loading: boolean; searchQuery: string; setSearchQuery: (query: string) => void; + onlineFilter: boolean; + setOnlineFilter: (filter: boolean) => void; selectedDb: string | null; setSelectedDb: (id: string | null) => void; status: Map; @@ -16,6 +19,9 @@ export interface ConnectionListProps { onDatabaseHover: (dbId: string) => void; onDelete: (dbId: string, dbName: string) => void; onTest: (dbId: string, dbName: string) => void; + onImportClick?: () => void; + onRelinkProject?: (projectId: string, newDatabaseId: string) => void; + onDeleteProject?: (projectId: string) => void; } export interface DatabaseDetailProps { @@ -37,11 +43,11 @@ export interface WelcomeViewProps { totalTables: number | string; totalSize: string; statsLoading: boolean; - welcomeMessage: string; onAddClick: () => void; onSelectDb: (id: string) => void; onDatabaseHover: (dbId: string) => void; onDiscoveredDatabaseAdd?: (db: DiscoveredDatabase) => void; + onOnlineFilterClick: () => void; } export interface AddConnectionDialogProps { diff --git a/src/features/home/utils.ts b/src/features/home/utils.ts deleted file mode 100644 index cd2b51f..0000000 --- a/src/features/home/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Format a date string to a relative time (e.g., "5m ago", "2h ago") - */ -export function formatRelativeTime(dateString?: string): string { - if (!dateString) return "Never"; - const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - if (diffMins < 1) return "Just now"; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); -} diff --git a/src/features/project/components/CreateProjectDialog.tsx b/src/features/project/components/CreateProjectDialog.tsx deleted file mode 100644 index 8aa54d0..0000000 --- a/src/features/project/components/CreateProjectDialog.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { useState } from "react"; -import { Database, Link as LinkIcon } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { DatabaseConnection } from "@/features/database/types"; - -interface CreateProjectDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onSubmit: (data: { - databaseId: string; - name: string; - description?: string; - defaultSchema?: string; - }) => void; - isLoading?: boolean; - databases: DatabaseConnection[]; -} - -export function CreateProjectDialog({ - open, - onOpenChange, - onSubmit, - isLoading, - databases, -}: CreateProjectDialogProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [databaseId, setDatabaseId] = useState(""); - const [defaultSchema, setDefaultSchema] = useState(""); - - const resetForm = () => { - setName(""); - setDescription(""); - setDatabaseId(""); - setDefaultSchema(""); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim() || !databaseId) return; - - onSubmit({ - databaseId, - name: name.trim(), - description: description.trim() || undefined, - defaultSchema: defaultSchema.trim() || undefined, - }); - - resetForm(); - }; - - return ( - { - onOpenChange(isOpen); - if (!isOpen) resetForm(); - }} - > - - - - - Create Project - - - Create a project to save schema, ER diagrams, and queries offline. - - - -
    - {/* Project Name */} -
    - - setName(e.target.value)} - autoFocus - /> -
    - - {/* Linked Database */} -
    - - -
    - - {/* Default Schema */} -
    - - setDefaultSchema(e.target.value)} - /> -
    - - {/* Description */} -
    - -