From a48a5a346277cb10fe6360693a080eda8ffa0508 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 12:36:05 -0500 Subject: [PATCH] core: migrate from custom JSON storage to standard Drizzle migrations to improve database reliability and performance This replaces the previous manual JSON file system with standard Drizzle migrations, enabling: - Proper database schema migrations with timestamp-based versioning - Batched migration for faster migration of large datasets - Better data integrity with proper table schemas instead of JSON blobs - Easier database upgrades and rollback capabilities Migration changes: - Todo table now uses individual columns with composite PK instead of JSON blob - Share table removes unused download share data - Session diff table moved from database table to file storage - All migrations now use proper Drizzle format with per-folder layout Users will see a one-time migration on next run that migrates existing JSON data to the new SQLite database. --- .opencode/opencode.jsonc | 6 - bun.lock | 177 +--- package.json | 2 + packages/console/core/package.json | 4 +- packages/opencode/AGENTS.md | 33 +- packages/opencode/drizzle.config.ts | 3 + .../migration.sql} | 58 +- .../snapshot.json | 787 ++++++++++++++++++ .../migration/meta/0000_snapshot.json | 587 ------------- .../opencode/migration/meta/_journal.json | 13 - packages/opencode/package.json | 6 +- packages/opencode/src/cli/cmd/database.ts | 144 ---- packages/opencode/src/index.ts | 13 +- packages/opencode/src/project/bootstrap.ts | 2 - packages/opencode/src/session/index.ts | 28 +- packages/opencode/src/session/revert.ts | 11 +- packages/opencode/src/session/session.sql.ts | 30 +- packages/opencode/src/session/summary.ts | 20 +- packages/opencode/src/session/todo.ts | 36 +- packages/opencode/src/share/share-next.ts | 10 +- packages/opencode/src/share/share.sql.ts | 14 +- packages/opencode/src/share/share.ts | 92 -- packages/opencode/src/storage/db.ts | 68 +- .../opencode/src/storage/json-migration.ts | 479 ++++++----- packages/opencode/src/storage/storage.ts | 227 +++++ .../test/storage/json-migration.test.ts | 56 +- 26 files changed, 1499 insertions(+), 1407 deletions(-) rename packages/opencode/migration/{0000_magical_strong_guy.sql => 20260127173238_melted_union_jack/migration.sql} (58%) create mode 100644 packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json delete mode 100644 packages/opencode/migration/meta/0000_snapshot.json delete mode 100644 packages/opencode/migration/meta/_journal.json delete mode 100644 packages/opencode/src/cli/cmd/database.ts delete mode 100644 packages/opencode/src/share/share.ts create mode 100644 packages/opencode/src/storage/storage.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index c3f0b7070d..2d726247c1 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -9,12 +9,6 @@ "options": {}, }, }, - "mcp": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/mcp", - }, - }, "tools": { "github-triage": false, "github-pr-search": false, diff --git a/bun.lock b/bun.lock index d015c9aea9..25724edd9e 100644 --- a/bun.lock +++ b/bun.lock @@ -115,7 +115,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -127,7 +127,7 @@ "@types/bun": "1.3.0", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", }, @@ -354,6 +354,7 @@ "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -524,6 +525,8 @@ "ai": "5.0.119", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -849,7 +852,7 @@ "@dot/log": ["@dot/log@0.1.5", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -861,10 +864,6 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -2353,9 +2352,9 @@ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2417,8 +2416,6 @@ "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -4127,8 +4124,6 @@ "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], "@gitlab/gitlab-ai-provider/openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="], @@ -4381,9 +4376,11 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "db0/drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], @@ -4469,10 +4466,6 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "opencode/drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - - "opencode/drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], - "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], @@ -4645,50 +4638,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], @@ -5017,51 +4966,55 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5085,10 +5038,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "opencode/drizzle-kit/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - - "opencode/drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], @@ -5315,56 +5264,6 @@ "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opencode/drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "opencode/drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], diff --git a/package.json b/package.json index 4267ef6456..0686c705ee 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "ai": "5.0.119", "hono": "4.10.7", "hono-openapi": "1.1.2", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 95f019b1cd..515ae15316 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -12,7 +12,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -43,7 +43,7 @@ "@tsconfig/node22": "22.0.2", "@types/bun": "1.3.0", "@types/node": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", "@typescript/native-preview": "catalog:" diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index a68fd7f3e3..dcfc336d65 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -1,27 +1,10 @@ -# opencode agent guidelines +# opencode database guide -## Build/Test Commands +## Database -- **Install**: `bun install` -- **Run**: `bun run --conditions=browser ./src/index.ts` -- **Typecheck**: `bun run typecheck` (npm run typecheck) -- **Test**: `bun test` (runs all tests) -- **Single test**: `bun test test/tool/tool.test.ts` (specific test file) - -## Code Style - -- **Runtime**: Bun with TypeScript ESM modules -- **Imports**: Use relative imports for local modules, named imports preferred -- **Types**: Zod schemas for validation, TypeScript interfaces for structure -- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces -- **Error handling**: Use Result patterns, avoid throwing exceptions in tools -- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`) - -## Architecture - -- **Tools**: Implement `Tool.Info` interface with `execute()` method -- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI -- **Validation**: All inputs validated with Zod schemas -- **Logging**: Use `Log.create({ service: "name" })` pattern -- **Storage**: Use `Storage` namespace for persistence -- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files. +- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`. +- **Naming**: tables and columns use snake*case; join columns are `_id`; indexes are `*\_idx`. +- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`). +- **Command**: `bun run db generate --name `. +- **Output**: creates `migration/_/migration.sql` and `snapshot.json`. +- **Tests**: migration tests should read the per-folder layout (no `_journal.json`). diff --git a/packages/opencode/drizzle.config.ts b/packages/opencode/drizzle.config.ts index 551a2384c5..1b4fd556e9 100644 --- a/packages/opencode/drizzle.config.ts +++ b/packages/opencode/drizzle.config.ts @@ -4,4 +4,7 @@ export default defineConfig({ dialect: "sqlite", schema: "./src/**/*.sql.ts", out: "./migration", + dbCredentials: { + url: "/home/thdxr/.local/share/opencode/opencode.db", + }, }) diff --git a/packages/opencode/migration/0000_magical_strong_guy.sql b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql similarity index 58% rename from packages/opencode/migration/0000_magical_strong_guy.sql rename to packages/opencode/migration/20260127173238_melted_union_jack/migration.sql index e25f0d3d56..bc17ef4938 100644 --- a/packages/opencode/migration/0000_magical_strong_guy.sql +++ b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql @@ -1,5 +1,5 @@ CREATE TABLE `project` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `worktree` text NOT NULL, `vcs` text, `name` text, @@ -12,38 +12,29 @@ CREATE TABLE `project` ( ); --> statement-breakpoint CREATE TABLE `message` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `session_id` text NOT NULL, `created_at` integer NOT NULL, `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint CREATE TABLE `part` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `message_id` text NOT NULL, `session_id` text NOT NULL, `data` text NOT NULL, - FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint -CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint CREATE TABLE `permission` ( - `project_id` text PRIMARY KEY NOT NULL, + `project_id` text PRIMARY KEY, `data` text NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `session_diff` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `project_id` text NOT NULL, `parent_id` text, `slug` text NOT NULL, @@ -64,24 +55,31 @@ CREATE TABLE `session` ( `time_updated` integer NOT NULL, `time_compacting` integer, `time_archived` integer, - FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint -CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint CREATE TABLE `todo` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + `session_id` text NOT NULL, + `id` text NOT NULL, + `content` text NOT NULL, + `status` text NOT NULL, + `priority` text NOT NULL, + `position` integer NOT NULL, + CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`), + CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `session_share` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + `session_id` text PRIMARY KEY, + `id` text NOT NULL, + `secret` text NOT NULL, + `url` text NOT NULL, + CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE TABLE `share` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL -); +CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint +CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint +CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint +CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint +CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint +CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); \ No newline at end of file diff --git a/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json b/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json new file mode 100644 index 0000000000..63943f49d3 --- /dev/null +++ b/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json @@ -0,0 +1,787 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "0e365b40-39c4-447f-9729-9714d865d8ff", + "prevIds": [ + "00000000-0000-0000-0000-000000000000" + ], + "ddl": [ + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_message_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_part_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_snapshot", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_diff", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "session_id", + "id" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/migration/meta/0000_snapshot.json b/packages/opencode/migration/meta/0000_snapshot.json deleted file mode 100644 index efec141ea6..0000000000 --- a/packages/opencode/migration/meta/0000_snapshot.json +++ /dev/null @@ -1,587 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "797eb060-2c45-4abf-925d-6b8375dd8a64", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "project": { - "name": "project", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "worktree": { - "name": "worktree", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "vcs": { - "name": "vcs", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon_url": { - "name": "icon_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon_color": { - "name": "icon_color", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_created": { - "name": "time_created", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_updated": { - "name": "time_updated", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_initialized": { - "name": "time_initialized", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sandboxes": { - "name": "sandboxes", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "message": { - "name": "message", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "message_session_idx": { - "name": "message_session_idx", - "columns": [ - "session_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "message_session_id_session_id_fk": { - "name": "message_session_id_session_id_fk", - "tableFrom": "message", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "part": { - "name": "part", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "message_id": { - "name": "message_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "part_message_idx": { - "name": "part_message_idx", - "columns": [ - "message_id" - ], - "isUnique": false - }, - "part_session_idx": { - "name": "part_session_idx", - "columns": [ - "session_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "part_message_id_message_id_fk": { - "name": "part_message_id_message_id_fk", - "tableFrom": "part", - "tableTo": "message", - "columnsFrom": [ - "message_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "permission": { - "name": "permission", - "columns": { - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "permission_project_id_project_id_fk": { - "name": "permission_project_id_project_id_fk", - "tableFrom": "permission", - "tableTo": "project", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session_diff": { - "name": "session_diff", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_diff_session_id_session_id_fk": { - "name": "session_diff_session_id_session_id_fk", - "tableFrom": "session_diff", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session": { - "name": "session", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "parent_id": { - "name": "parent_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "directory": { - "name": "directory", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "share_url": { - "name": "share_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_additions": { - "name": "summary_additions", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_deletions": { - "name": "summary_deletions", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_files": { - "name": "summary_files", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_diffs": { - "name": "summary_diffs", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_message_id": { - "name": "revert_message_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_part_id": { - "name": "revert_part_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_snapshot": { - "name": "revert_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_diff": { - "name": "revert_diff", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "permission": { - "name": "permission", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_created": { - "name": "time_created", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_updated": { - "name": "time_updated", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_compacting": { - "name": "time_compacting", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_archived": { - "name": "time_archived", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "session_project_idx": { - "name": "session_project_idx", - "columns": [ - "project_id" - ], - "isUnique": false - }, - "session_parent_idx": { - "name": "session_parent_idx", - "columns": [ - "parent_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "session_project_id_project_id_fk": { - "name": "session_project_id_project_id_fk", - "tableFrom": "session", - "tableTo": "project", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "todo": { - "name": "todo", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "todo_session_id_session_id_fk": { - "name": "todo_session_id_session_id_fk", - "tableFrom": "todo", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session_share": { - "name": "session_share", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_share_session_id_session_id_fk": { - "name": "session_share_session_id_session_id_fk", - "tableFrom": "session_share", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "share": { - "name": "share", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/packages/opencode/migration/meta/_journal.json b/packages/opencode/migration/meta/_journal.json deleted file mode 100644 index 4ab81e184d..0000000000 --- a/packages/opencode/migration/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1769232577135, - "tag": "0000_magical_strong_guy", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c227eac191..1d1532598d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -26,7 +26,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "drizzle-kit": "1.0.0-beta.12-a5629fb", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", @@ -43,6 +42,8 @@ "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -122,5 +123,8 @@ "yargs": "18.0.0", "zod": "catalog:", "zod-to-json-schema": "3.24.5" + }, + "overrides": { + "drizzle-orm": "1.0.0-beta.12-a5629fb" } } diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts deleted file mode 100644 index 44e7295053..0000000000 --- a/packages/opencode/src/cli/cmd/database.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { Argv } from "yargs" -import { cmd } from "./cmd" -import { bootstrap } from "../bootstrap" -import { UI } from "../ui" -import { Database } from "../../storage/db" -import { ProjectTable } from "../../project/project.sql" -import { Project } from "../../project/project" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../../session/session.sql" -import { Session } from "../../session" -import { SessionShareTable, ShareTable } from "../../share/share.sql" -import path from "path" -import fs from "fs/promises" - -export const DatabaseCommand = cmd({ - command: "database", - describe: "database management commands", - builder: (yargs) => yargs.command(ExportCommand).demandCommand(), - async handler() {}, -}) - -const ExportCommand = cmd({ - command: "export", - describe: "export database to JSON files", - builder: (yargs: Argv) => { - return yargs.option("output", { - alias: ["o"], - describe: "output directory", - type: "string", - demandOption: true, - }) - }, - handler: async (args) => { - await bootstrap(process.cwd(), async () => { - const outDir = path.resolve(args.output) - await fs.mkdir(outDir, { recursive: true }) - - const stats = { - projects: 0, - sessions: 0, - messages: 0, - parts: 0, - diffs: 0, - todos: 0, - permissions: 0, - sessionShares: 0, - shares: 0, - } - - // Export projects - const projectDir = path.join(outDir, "project") - await fs.mkdir(projectDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(ProjectTable).all())) { - const project = Project.fromRow(row) - await Bun.write(path.join(projectDir, `${row.id}.json`), JSON.stringify(project, null, 2)) - stats.projects++ - } - - // Export sessions (organized by projectID) - const sessionDir = path.join(outDir, "session") - for (const row of Database.use((db) => db.select().from(SessionTable).all())) { - const dir = path.join(sessionDir, row.project_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(Session.fromRow(row), null, 2)) - stats.sessions++ - } - - // Export messages (organized by sessionID) - const messageDir = path.join(outDir, "message") - for (const row of Database.use((db) => db.select().from(MessageTable).all())) { - const dir = path.join(messageDir, row.session_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) - stats.messages++ - } - - // Export parts (organized by messageID) - const partDir = path.join(outDir, "part") - for (const row of Database.use((db) => db.select().from(PartTable).all())) { - const dir = path.join(partDir, row.message_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) - stats.parts++ - } - - // Export session diffs - const diffDir = path.join(outDir, "session_diff") - await fs.mkdir(diffDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(SessionDiffTable).all())) { - await Bun.write(path.join(diffDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.diffs++ - } - - // Export todos - const todoDir = path.join(outDir, "todo") - await fs.mkdir(todoDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(TodoTable).all())) { - await Bun.write(path.join(todoDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.todos++ - } - - // Export permissions - const permDir = path.join(outDir, "permission") - await fs.mkdir(permDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(PermissionTable).all())) { - await Bun.write(path.join(permDir, `${row.project_id}.json`), JSON.stringify(row.data, null, 2)) - stats.permissions++ - } - - // Export session shares - const sessionShareDir = path.join(outDir, "session_share") - await fs.mkdir(sessionShareDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(SessionShareTable).all())) { - await Bun.write(path.join(sessionShareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.sessionShares++ - } - - // Export shares - const shareDir = path.join(outDir, "share") - await fs.mkdir(shareDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(ShareTable).all())) { - await Bun.write(path.join(shareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.shares++ - } - - UI.println(`Exported to ${outDir}:`) - UI.println(` ${stats.projects} projects`) - UI.println(` ${stats.sessions} sessions`) - UI.println(` ${stats.messages} messages`) - UI.println(` ${stats.parts} parts`) - UI.println(` ${stats.diffs} session diffs`) - UI.println(` ${stats.todos} todos`) - UI.println(` ${stats.permissions} permissions`) - UI.println(` ${stats.sessionShares} session shares`) - UI.println(` ${stats.shares} shares`) - }) - }, -}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index e73fda21b7..a929f675d6 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,7 +26,10 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" -import { DatabaseCommand } from "./cli/cmd/database" +import path from "path" +import { Global } from "./global" +import { JsonMigration } from "./storage/json-migration" +import { Database } from "./storage/db" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -75,6 +78,13 @@ const cli = yargs(hideBin(process.argv)) version: Installation.VERSION, args: process.argv.slice(2), }) + + const marker = path.join(Global.Path.data, "opencode.db") + if (!(await Bun.file(marker).exists())) { + console.log("Performing one time database migration, may take a few minutes...") + await JsonMigration.run(Database.Client().$client) + console.log("Database migration complete.") + } }) .usage("\n" + UI.logo()) .completion("completion", "generate shell completion script") @@ -98,7 +108,6 @@ const cli = yargs(hideBin(process.argv)) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) - .command(DatabaseCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index efdcaba990..a2be3733f8 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -1,5 +1,4 @@ import { Plugin } from "../plugin" -import { Share } from "../share/share" import { Format } from "../format" import { LSP } from "../lsp" import { FileWatcher } from "../file/watcher" @@ -17,7 +16,6 @@ import { Truncate } from "../tool/truncation" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) await Plugin.init() - Share.init() ShareNext.init() Format.init() await LSP.init() diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 443c47a534..4085d99a37 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -11,8 +11,8 @@ import { Identifier } from "../id/id" import { Installation } from "../installation" import { Database, NotFoundError, eq } from "../storage/db" -import { SessionTable, MessageTable, PartTable, SessionDiffTable } from "./session.sql" -import { ShareTable } from "../share/share.sql" +import { SessionTable, MessageTable, PartTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" @@ -153,16 +153,6 @@ export namespace Session { }) export type Info = z.output - export const ShareInfo = z - .object({ - secret: z.string(), - url: z.string(), - }) - .meta({ - ref: "SessionShare", - }) - export type ShareInfo = z.output - export const Event = { Created: BusEvent.define( "session.created", @@ -323,11 +313,6 @@ export namespace Session { return fromRow(row) }) - export const getShare = fn(Identifier.schema("session"), async (id) => { - const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.session_id, id)).get()) - return row?.data - }) - export const share = fn(Identifier.schema("session"), async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { @@ -498,10 +483,11 @@ export namespace Session { ) export const diff = fn(Identifier.schema("session"), async (sessionID) => { - const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, sessionID)).get(), - ) - return row?.data ?? [] + try { + return await Storage.read(["session_diff", sessionID]) + } catch { + return [] + } }) export const messages = fn( diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index d402310c3e..ef9c7e2aac 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -5,7 +5,8 @@ import { MessageV2 } from "./message-v2" import { Session } from "." import { Log } from "../util/log" import { Database, eq } from "../storage/db" -import { SessionDiffTable, MessageTable, PartTable } from "./session.sql" +import { MessageTable, PartTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Bus } from "../bus" import { SessionPrompt } from "./prompt" import { SessionSummary } from "./summary" @@ -60,13 +61,7 @@ export namespace SessionRevert { if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot) const rangeMessages = all.filter((msg) => msg.info.id >= revert!.messageID) const diffs = await SessionSummary.computeDiff({ messages: rangeMessages }) - Database.use((db) => - db - .insert(SessionDiffTable) - .values({ session_id: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) - .run(), - ) + await Storage.write(["session_diff", input.sessionID], diffs) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 7d52d87a4b..2afaef5aa0 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -1,8 +1,7 @@ -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "@/snapshot" -import type { Todo } from "./todo" import type { PermissionNext } from "@/permission/next" export const SessionTable = sqliteTable( @@ -61,19 +60,20 @@ export const PartTable = sqliteTable( (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], ) -export const SessionDiffTable = sqliteTable("session_diff", { - session_id: text() - .primaryKey() - .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type(), -}) - -export const TodoTable = sqliteTable("todo", { - session_id: text() - .primaryKey() - .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type(), -}) +export const TodoTable = sqliteTable( + "todo", + { + session_id: text() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + id: text().notNull(), + content: text().notNull(), + status: text().notNull(), + priority: text().notNull(), + position: integer().notNull(), + }, + (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], +) export const PermissionTable = sqliteTable("permission", { project_id: text() diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 0183a9d3df..53a52af331 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -11,8 +11,7 @@ import { Snapshot } from "@/snapshot" import { Log } from "@/util/log" import path from "path" import { Instance } from "@/project/instance" -import { Database, eq } from "@/storage/db" -import { SessionDiffTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Bus } from "@/bus" import { LLM } from "./llm" @@ -56,13 +55,7 @@ export namespace SessionSummary { files: diffs.length, }, }) - Database.use((db) => - db - .insert(SessionDiffTable) - .values({ session_id: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) - .run(), - ) + await Storage.write(["session_diff", input.sessionID], diffs) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -124,10 +117,11 @@ export namespace SessionSummary { messageID: Identifier.schema("message").optional(), }), async (input) => { - const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, input.sessionID)).get(), - ) - return row?.data ?? [] + try { + return await Storage.read(["session_diff", input.sessionID]) + } catch { + return [] + } }, ) diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 03bbcc148e..6ef7cbaaf5 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" -import { Database, eq } from "../storage/db" +import { Database, eq, asc } from "../storage/db" import { TodoTable } from "./session.sql" export namespace Todo { @@ -26,18 +26,34 @@ export namespace Todo { } export function update(input: { sessionID: string; todos: Info[] }) { - Database.use((db) => - db - .insert(TodoTable) - .values({ session_id: input.sessionID, data: input.todos }) - .onConflictDoUpdate({ target: TodoTable.session_id, set: { data: input.todos } }) - .run(), - ) + Database.transaction((db) => { + db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() + if (input.todos.length === 0) return + db.insert(TodoTable) + .values( + input.todos.map((todo, position) => ({ + session_id: input.sessionID, + id: todo.id, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + })), + ) + .run() + }) Bus.publish(Event.Updated, input) } export function get(sessionID: string) { - const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).get()) - return row?.data ?? [] + const rows = Database.use((db) => + db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(), + ) + return rows.map((row) => ({ + id: row.id, + content: row.content, + status: row.status, + priority: row.priority, + })) } } diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 0cf978930e..108db444c8 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -81,8 +81,11 @@ export namespace ShareNext { Database.use((db) => db .insert(SessionShareTable) - .values({ session_id: sessionID, data: result }) - .onConflictDoUpdate({ target: SessionShareTable.session_id, set: { data: result } }) + .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) + .onConflictDoUpdate({ + target: SessionShareTable.session_id, + set: { id: result.id, secret: result.secret, url: result.url }, + }) .run(), ) fullSync(sessionID) @@ -93,7 +96,8 @@ export namespace ShareNext { const row = Database.use((db) => db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), ) - return row?.data + if (!row) return + return { id: row.id, secret: row.secret, url: row.url } } type Data = diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index bf8a190461..4d9c9290a5 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,19 +1,11 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" -import type { Session } from "../session" export const SessionShareTable = sqliteTable("session_share", { session_id: text() .primaryKey() .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type<{ - id: string - secret: string - url: string - }>(), -}) - -export const ShareTable = sqliteTable("share", { - session_id: text().primaryKey(), - data: text({ mode: "json" }).notNull().$type(), + id: text().notNull(), + secret: text().notNull(), + url: text().notNull(), }) diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts deleted file mode 100644 index f7bf4b3fa5..0000000000 --- a/packages/opencode/src/share/share.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Bus } from "../bus" -import { Installation } from "../installation" -import { Session } from "../session" -import { MessageV2 } from "../session/message-v2" -import { Log } from "../util/log" - -export namespace Share { - const log = Log.create({ service: "share" }) - - let queue: Promise = Promise.resolve() - const pending = new Map() - - export async function sync(key: string, content: any) { - if (disabled) return - const [root, ...splits] = key.split("/") - if (root !== "session") return - const [sub, sessionID] = splits - if (sub === "share") return - const share = await Session.getShare(sessionID).catch(() => {}) - if (!share) return - const { secret } = share - pending.set(key, content) - queue = queue - .then(async () => { - const content = pending.get(key) - if (content === undefined) return - pending.delete(key) - - return fetch(`${URL}/share_sync`, { - method: "POST", - body: JSON.stringify({ - sessionID: sessionID, - secret, - key: key, - content, - }), - }) - }) - .then((x) => { - if (x) { - log.info("synced", { - key: key, - status: x.status, - }) - } - }) - } - - export function init() { - Bus.subscribe(Session.Event.Updated, async (evt) => { - await sync("session/info/" + evt.properties.info.id, evt.properties.info) - }) - Bus.subscribe(MessageV2.Event.Updated, async (evt) => { - await sync("session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id, evt.properties.info) - }) - Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { - await sync( - "session/part/" + - evt.properties.part.sessionID + - "/" + - evt.properties.part.messageID + - "/" + - evt.properties.part.id, - evt.properties.part, - ) - }) - } - - export const URL = - process.env["OPENCODE_API"] ?? - (Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai") - - const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" - - export async function create(sessionID: string) { - if (disabled) return { url: "", secret: "" } - return fetch(`${URL}/share_create`, { - method: "POST", - body: JSON.stringify({ sessionID: sessionID }), - }) - .then((x) => x.json()) - .then((x) => x as { url: string; secret: string }) - } - - export async function remove(sessionID: string, secret: string) { - if (disabled) return {} - return fetch(`${URL}/share_delete`, { - method: "POST", - body: JSON.stringify({ sessionID, secret }), - }).then((x) => x.json()) - } -} diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index f49028a18b..0beddca8f2 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -7,11 +7,12 @@ import { Context } from "../util/context" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util/log" -import { migrateFromJson } from "./json-migration" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" -import { readFileSync } from "fs" +import { readFileSync, readdirSync } from "fs" +import fs from "fs/promises" +import { Instance } from "@/project/instance" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined @@ -31,21 +32,39 @@ export namespace Database { type Journal = { sql: string; timestamp: number }[] - function journal(dir: string): Journal { - const file = path.join(dir, "meta/_journal.json") - if (!Bun.file(file).size) return [] - - const data = JSON.parse(readFileSync(file, "utf-8")) as { - entries: { tag: string; when: number }[] - } - - return data.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) + function time(tag: string) { + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) + if (!match) return 0 + return Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) } - const client = lazy(() => { + function migrations(dir: string): Journal { + const dirs = readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + + const sql = dirs + .map((name) => { + const file = path.join(dir, name, "migration.sql") + if (!Bun.file(file).size) return + return { + sql: readFileSync(file, "utf-8"), + timestamp: time(name), + } + }) + .filter(Boolean) as Journal + + return sql.sort((a, b) => a.timestamp - b.timestamp) + } + + export const Client = lazy(() => { log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true }) @@ -62,7 +81,7 @@ export namespace Database { const entries = typeof OPENCODE_MIGRATIONS !== "undefined" ? OPENCODE_MIGRATIONS - : journal(path.join(import.meta.dirname, "../../migration")) + : migrations(path.join(import.meta.dirname, "../../migration")) if (entries.length > 0) { log.info("applying migrations", { count: entries.length, @@ -71,19 +90,6 @@ export namespace Database { migrate(db, entries) } - // Run json migration if not already done - if (!sqlite.prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = 'json-migration'").get()) { - Bun.file(path.join(Global.Path.data, "storage/project")) - .exists() - .then((exists) => { - if (!exists) return - return migrateFromJson(sqlite).then(() => { - sqlite.run("INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('json-migration', ?)", [Date.now()]) - }) - }) - .catch((e) => log.error("json migration failed", { error: e })) - } - return db }) @@ -100,7 +106,7 @@ export namespace Database { } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const result = ctx.provide({ effects, tx: client() }, () => callback(client())) + const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) for (const effect of effects) effect() return result } @@ -122,7 +128,7 @@ export namespace Database { } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const result = client().transaction((tx) => { + const result = Client().transaction((tx) => { return ctx.provide({ tx, effects }, () => callback(tx)) }) for (const effect of effects) effect() diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 4936387fd7..4b235a9dc0 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -1,51 +1,70 @@ import { Database } from "bun:sqlite" import { drizzle } from "drizzle-orm/bun-sqlite" -import { eq } from "drizzle-orm" import { Global } from "../global" import { Log } from "../util/log" import { ProjectTable } from "../project/project.sql" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../session/session.sql" -import { SessionShareTable, ShareTable } from "../share/share.sql" +import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" +import { SessionShareTable } from "../share/share.sql" import path from "path" -const log = Log.create({ service: "json-migration" }) +export namespace JsonMigration { + const log = Log.create({ service: "json-migration" }) -export async function migrateFromJson(sqlite: Database, customStorageDir?: string) { - const storageDir = customStorageDir ?? path.join(Global.Path.data, "storage") + export async function run(sqlite: Database) { + const storageDir = path.join(Global.Path.data, "storage") - log.info("starting json to sqlite migration", { storageDir }) + log.info("starting json to sqlite migration", { storageDir }) - const db = drizzle({ client: sqlite }) - const stats = { - projects: 0, - sessions: 0, - messages: 0, - parts: 0, - diffs: 0, - todos: 0, - permissions: 0, - shares: 0, - errors: [] as string[], - } + const db = drizzle({ client: sqlite }) + const stats = { + projects: 0, + sessions: 0, + messages: 0, + parts: 0, + todos: 0, + permissions: 0, + shares: 0, + errors: [] as string[], + } - // Migrate projects first (no FK deps) - const projectGlob = new Bun.Glob("project/*.json") - for await (const file of projectGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id) { - stats.errors.push(`project missing id: ${file}`) - continue + const limit = 32 + + async function list(pattern: string) { + const items: string[] = [] + const scan = new Bun.Glob(pattern) + for await (const file of scan.scan({ cwd: storageDir, absolute: true })) { + items.push(file) } - db.insert(ProjectTable) - .values({ + return items + } + + async function read(files: string[]) { + const results = await Promise.allSettled(files.map((file) => Bun.file(file).json())) + const items: { file: string; data: any }[] = [] + for (let i = 0; i < results.length; i++) { + const result = results[i] + const file = files[i] + if (result.status === "fulfilled") { + items.push({ file, data: result.value }) + continue + } + stats.errors.push(`failed to read ${file}: ${result.reason}`) + } + return items + } + + // Migrate projects first (no FK deps) + const projectFiles = await list("project/*.json") + for (let i = 0; i < projectFiles.length; i += limit) { + const batch = await read(projectFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + if (!data?.id) { + stats.errors.push(`project missing id: ${item.file}`) + continue + } + values.push({ id: data.id, worktree: data.worktree ?? "/", vcs: data.vcs, @@ -57,32 +76,36 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin time_initialized: data.time?.initialized, sandboxes: data.sandboxes ?? [], }) - .onConflictDoNothing() - .run() - stats.projects++ - } catch (e) { - stats.errors.push(`failed to migrate project ${file}: ${e}`) + } + if (values.length === 0) continue + try { + db.insert(ProjectTable).values(values).onConflictDoNothing().run() + stats.projects += values.length + } catch (e) { + stats.errors.push(`failed to migrate project batch: ${e}`) + } } - } - log.info("migrated projects", { count: stats.projects }) + log.info("migrated projects", { count: stats.projects }) - // Migrate sessions (depends on projects) - const sessionGlob = new Bun.Glob("session/*/*.json") - for await (const file of sessionGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.projectID) { - stats.errors.push(`session missing id or projectID: ${file}`) - continue - } - // Check if project exists (skip orphaned sessions) - const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, data.projectID)).get() - if (!project) { - log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID }) - continue - } - db.insert(SessionTable) - .values({ + const projectRows = db.select({ id: ProjectTable.id }).from(ProjectTable).all() + const projectIds = new Set(projectRows.map((item) => item.id)) + + // Migrate sessions (depends on projects) + const sessionFiles = await list("session/*/*.json") + for (let i = 0; i < sessionFiles.length; i += limit) { + const batch = await read(sessionFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + if (!data?.id || !data?.projectID) { + stats.errors.push(`session missing id or projectID: ${item.file}`) + continue + } + if (!projectIds.has(data.projectID)) { + log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID }) + continue + } + values.push({ id: data.id, project_id: data.projectID, parent_id: data.parentID ?? null, @@ -105,181 +128,199 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin time_compacting: data.time?.compacting ?? null, time_archived: data.time?.archived ?? null, }) - .onConflictDoNothing() - .run() - stats.sessions++ - } catch (e) { - stats.errors.push(`failed to migrate session ${file}: ${e}`) - } - } - log.info("migrated sessions", { count: stats.sessions }) - - // Migrate messages (depends on sessions) - const messageGlob = new Bun.Glob("message/*/*.json") - for await (const file of messageGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.sessionID) { - stats.errors.push(`message missing id or sessionID: ${file}`) - continue } - // Check if session exists - const session = db.select().from(SessionTable).where(eq(SessionTable.id, data.sessionID)).get() - if (!session) { - log.warn("skipping orphaned message", { messageID: data.id, sessionID: data.sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(SessionTable).values(values).onConflictDoNothing().run() + stats.sessions += values.length + } catch (e) { + stats.errors.push(`failed to migrate session batch: ${e}`) } - db.insert(MessageTable) - .values({ - id: data.id, - session_id: data.sessionID, - created_at: data.time?.created ?? Date.now(), - data, - }) - .onConflictDoNothing() - .run() - stats.messages++ - } catch (e) { - stats.errors.push(`failed to migrate message ${file}: ${e}`) } - } - log.info("migrated messages", { count: stats.messages }) + log.info("migrated sessions", { count: stats.sessions }) - // Migrate parts (depends on messages) - const partGlob = new Bun.Glob("part/*/*.json") - for await (const file of partGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.messageID || !data.sessionID) { - stats.errors.push(`part missing id, messageID, or sessionID: ${file}`) - continue + const sessionRows = db.select({ id: SessionTable.id }).from(SessionTable).all() + const sessionIds = new Set(sessionRows.map((item) => item.id)) + + // Migrate messages + parts per session + const sessionList = Array.from(sessionIds) + for (let i = 0; i < sessionList.length; i += limit) { + const batch = sessionList.slice(i, i + limit) + await Promise.allSettled( + batch.map(async (sessionID) => { + const messageFiles = await list(`message/${sessionID}/*.json`) + const messageIds = new Set() + for (let j = 0; j < messageFiles.length; j += limit) { + const chunk = await read(messageFiles.slice(j, j + limit)) + const values = [] as any[] + for (const item of chunk) { + const data = item.data + if (!data?.id) { + stats.errors.push(`message missing id: ${item.file}`) + continue + } + values.push({ + id: data.id, + session_id: sessionID, + created_at: data.time?.created ?? Date.now(), + data, + }) + messageIds.add(data.id) + } + if (values.length === 0) continue + try { + db.insert(MessageTable).values(values).onConflictDoNothing().run() + stats.messages += values.length + } catch (e) { + stats.errors.push(`failed to migrate message batch: ${e}`) + } + } + + const messageList = Array.from(messageIds) + for (let j = 0; j < messageList.length; j += limit) { + const messageBatch = messageList.slice(j, j + limit) + await Promise.allSettled( + messageBatch.map(async (messageID) => { + const partFiles = await list(`part/${messageID}/*.json`) + for (let k = 0; k < partFiles.length; k += limit) { + const chunk = await read(partFiles.slice(k, k + limit)) + const values = [] as any[] + for (const item of chunk) { + const data = item.data + if (!data?.id || !data?.messageID) { + stats.errors.push(`part missing id or messageID: ${item.file}`) + continue + } + values.push({ + id: data.id, + message_id: data.messageID, + session_id: sessionID, + data, + }) + } + if (values.length === 0) continue + try { + db.insert(PartTable).values(values).onConflictDoNothing().run() + stats.parts += values.length + } catch (e) { + stats.errors.push(`failed to migrate part batch: ${e}`) + } + } + }), + ) + } + }), + ) + } + log.info("migrated messages", { count: stats.messages }) + log.info("migrated parts", { count: stats.parts }) + + // Migrate todos + const todoFiles = await list("todo/*.json") + for (let i = 0; i < todoFiles.length; i += limit) { + const batch = await read(todoFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const sessionID = path.basename(item.file, ".json") + if (!sessionIds.has(sessionID)) { + log.warn("skipping orphaned todo", { sessionID }) + continue + } + if (!Array.isArray(data)) { + stats.errors.push(`todo not an array: ${item.file}`) + continue + } + for (let position = 0; position < data.length; position++) { + const todo = data[position] + if (!todo?.id || !todo?.content || !todo?.status || !todo?.priority) continue + values.push({ + session_id: sessionID, + id: todo.id, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + }) + } } - // Check if message exists - const message = db.select().from(MessageTable).where(eq(MessageTable.id, data.messageID)).get() - if (!message) { - log.warn("skipping orphaned part", { partID: data.id, messageID: data.messageID }) - continue + if (values.length === 0) continue + try { + db.insert(TodoTable).values(values).onConflictDoNothing().run() + stats.todos += values.length + } catch (e) { + stats.errors.push(`failed to migrate todo batch: ${e}`) } - db.insert(PartTable) - .values({ - id: data.id, - message_id: data.messageID, - session_id: data.sessionID, - data, - }) - .onConflictDoNothing() - .run() - stats.parts++ - } catch (e) { - stats.errors.push(`failed to migrate part ${file}: ${e}`) } - } - log.info("migrated parts", { count: stats.parts }) + log.info("migrated todos", { count: stats.todos }) - // Migrate session diffs - const diffGlob = new Bun.Glob("session_diff/*.json") - for await (const file of diffGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - // Check if session exists - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned session_diff", { sessionID }) - continue + // Migrate permissions + const permFiles = await list("permission/*.json") + for (let i = 0; i < permFiles.length; i += limit) { + const batch = await read(permFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const projectID = path.basename(item.file, ".json") + if (!projectIds.has(projectID)) { + log.warn("skipping orphaned permission", { projectID }) + continue + } + values.push({ project_id: projectID, data }) } - db.insert(SessionDiffTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.diffs++ - } catch (e) { - stats.errors.push(`failed to migrate session_diff ${file}: ${e}`) - } - } - log.info("migrated session diffs", { count: stats.diffs }) - - // Migrate todos - const todoGlob = new Bun.Glob("todo/*.json") - for await (const file of todoGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned todo", { sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(PermissionTable).values(values).onConflictDoNothing().run() + stats.permissions += values.length + } catch (e) { + stats.errors.push(`failed to migrate permission batch: ${e}`) } - db.insert(TodoTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.todos++ - } catch (e) { - stats.errors.push(`failed to migrate todo ${file}: ${e}`) } - } - log.info("migrated todos", { count: stats.todos }) + log.info("migrated permissions", { count: stats.permissions }) - // Migrate permissions - const permGlob = new Bun.Glob("permission/*.json") - for await (const file of permGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const projectID = path.basename(file, ".json") - const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() - if (!project) { - log.warn("skipping orphaned permission", { projectID }) - continue + // Migrate session shares + const shareFiles = await list("session_share/*.json") + for (let i = 0; i < shareFiles.length; i += limit) { + const batch = await read(shareFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const sessionID = path.basename(item.file, ".json") + if (!sessionIds.has(sessionID)) { + log.warn("skipping orphaned session_share", { sessionID }) + continue + } + if (!data?.id || !data?.secret || !data?.url) { + stats.errors.push(`session_share missing id/secret/url: ${item.file}`) + continue + } + values.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url }) } - db.insert(PermissionTable).values({ project_id: projectID, data }).onConflictDoNothing().run() - stats.permissions++ - } catch (e) { - stats.errors.push(`failed to migrate permission ${file}: ${e}`) - } - } - log.info("migrated permissions", { count: stats.permissions }) - - // Migrate session shares - const shareGlob = new Bun.Glob("session_share/*.json") - for await (const file of shareGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned session_share", { sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(SessionShareTable).values(values).onConflictDoNothing().run() + stats.shares += values.length + } catch (e) { + stats.errors.push(`failed to migrate session_share batch: ${e}`) } - db.insert(SessionShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.shares++ - } catch (e) { - stats.errors.push(`failed to migrate session_share ${file}: ${e}`) } - } - log.info("migrated session shares", { count: stats.shares }) + log.info("migrated session shares", { count: stats.shares }) - // Migrate shares (downloaded shared sessions, no FK) - const share2Glob = new Bun.Glob("share/*.json") - for await (const file of share2Glob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - db.insert(ShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - } catch (e) { - stats.errors.push(`failed to migrate share ${file}: ${e}`) + log.info("json migration complete", { + projects: stats.projects, + sessions: stats.sessions, + messages: stats.messages, + parts: stats.parts, + todos: stats.todos, + permissions: stats.permissions, + shares: stats.shares, + errorCount: stats.errors.length, + }) + + if (stats.errors.length > 0) { + log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) } + + return stats } - - log.info("json migration complete", { - projects: stats.projects, - sessions: stats.sessions, - messages: stats.messages, - parts: stats.parts, - diffs: stats.diffs, - todos: stats.todos, - permissions: stats.permissions, - shares: stats.shares, - errorCount: stats.errors.length, - }) - - if (stats.errors.length > 0) { - log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) - } - - return stats } diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts new file mode 100644 index 0000000000..18f2d67e7a --- /dev/null +++ b/packages/opencode/src/storage/storage.ts @@ -0,0 +1,227 @@ +import { Log } from "../util/log" +import path from "path" +import fs from "fs/promises" +import { Global } from "../global" +import { Filesystem } from "../util/filesystem" +import { lazy } from "../util/lazy" +import { Lock } from "../util/lock" +import { $ } from "bun" +import { NamedError } from "@opencode-ai/util/error" +import z from "zod" + +export namespace Storage { + const log = Log.create({ service: "storage" }) + + type Migration = (dir: string) => Promise + + export const NotFoundError = NamedError.create( + "NotFoundError", + z.object({ + message: z.string(), + }), + ) + + const MIGRATIONS: Migration[] = [ + async (dir) => { + const project = path.resolve(dir, "../project") + if (!(await Filesystem.isDir(project))) return + for await (const projectDir of new Bun.Glob("*").scan({ + cwd: project, + onlyFiles: false, + })) { + log.info(`migrating project ${projectDir}`) + let projectID = projectDir + const fullProjectDir = path.join(project, projectDir) + let worktree = "/" + + if (projectID !== "global") { + for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ + cwd: path.join(project, projectDir), + absolute: true, + })) { + const json = await Bun.file(msgFile).json() + worktree = json.path?.root + if (worktree) break + } + if (!worktree) continue + if (!(await Filesystem.isDir(worktree))) continue + const [id] = await $`git rev-list --max-parents=0 --all` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => + x + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted(), + ) + if (!id) continue + projectID = id + + await Bun.write( + path.join(dir, "project", projectID + ".json"), + JSON.stringify({ + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }), + ) + + log.info(`migrating sessions for project ${projectID}`) + for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "session", projectID, path.basename(sessionFile)) + log.info("copying", { + sessionFile, + dest, + }) + const session = await Bun.file(sessionFile).json() + await Bun.write(dest, JSON.stringify(session)) + log.info(`migrating messages for session ${session.id}`) + for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "message", session.id, path.basename(msgFile)) + log.info("copying", { + msgFile, + dest, + }) + const message = await Bun.file(msgFile).json() + await Bun.write(dest, JSON.stringify(message)) + + log.info(`migrating parts for message ${message.id}`) + for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( + { + cwd: fullProjectDir, + absolute: true, + }, + )) { + const dest = path.join(dir, "part", message.id, path.basename(partFile)) + const part = await Bun.file(partFile).json() + log.info("copying", { + partFile, + dest, + }) + await Bun.write(dest, JSON.stringify(part)) + } + } + } + } + } + }, + async (dir) => { + for await (const item of new Bun.Glob("session/*/*.json").scan({ + cwd: dir, + absolute: true, + })) { + const session = await Bun.file(item).json() + if (!session.projectID) continue + if (!session.summary?.diffs) continue + const { diffs } = session.summary + await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs)) + await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write( + JSON.stringify({ + ...session, + summary: { + additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), + deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), + }, + }), + ) + } + }, + ] + + const state = lazy(async () => { + const dir = path.join(Global.Path.data, "storage") + const migration = await Bun.file(path.join(dir, "migration")) + .json() + .then((x) => parseInt(x)) + .catch(() => 0) + for (let index = migration; index < MIGRATIONS.length; index++) { + log.info("running migration", { index }) + const migration = MIGRATIONS[index] + await migration(dir).catch(() => log.error("failed to run migration", { index })) + await Bun.write(path.join(dir, "migration"), (index + 1).toString()) + } + return { + dir, + } + }) + + export async function remove(key: string[]) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + await fs.unlink(target).catch(() => {}) + }) + } + + export async function read(key: string[]) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.read(target) + const result = await Bun.file(target).json() + return result as T + }) + } + + export async function update(key: string[], fn: (draft: T) => void) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.write(target) + const content = await Bun.file(target).json() + fn(content) + await Bun.write(target, JSON.stringify(content, null, 2)) + return content as T + }) + } + + export async function write(key: string[], content: T) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.write(target) + await Bun.write(target, JSON.stringify(content, null, 2)) + }) + } + + async function withErrorHandling(body: () => Promise) { + return body().catch((e) => { + if (!(e instanceof Error)) throw e + const errnoException = e as NodeJS.ErrnoException + if (errnoException.code === "ENOENT") { + throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` }) + } + throw e + }) + } + + const glob = new Bun.Glob("**/*") + export async function list(prefix: string[]) { + const dir = await state().then((x) => x.dir) + try { + const result = await Array.fromAsync( + glob.scan({ + cwd: path.join(dir, ...prefix), + onlyFiles: true, + }), + ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) + result.sort() + return result + } catch { + return [] + } + } +} diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 70582eed04..bc55124002 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -5,20 +5,12 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator" import { eq } from "drizzle-orm" import path from "path" import fs from "fs/promises" -import { readFileSync } from "fs" -import os from "os" -import { migrateFromJson } from "../../src/storage/json-migration" +import { readFileSync, readdirSync } from "fs" +import { JsonMigration } from "../../src/storage/json-migration" +import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" import { Project } from "../../src/project/project" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../../src/session/session.sql" -import { SessionShareTable, ShareTable } from "../../src/share/share.sql" +import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" // Test fixtures const fixtures = { @@ -56,8 +48,9 @@ const fixtures = { } // Helper to create test storage directory structure -async function setupStorageDir(baseDir: string) { - const storageDir = path.join(baseDir, "storage") +async function setupStorageDir() { + const storageDir = path.join(Global.Path.data, "storage") + await fs.rm(storageDir, { recursive: true, force: true }) await fs.mkdir(path.join(storageDir, "project"), { recursive: true }) await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true }) await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true }) @@ -66,7 +59,6 @@ async function setupStorageDir(baseDir: string) { await fs.mkdir(path.join(storageDir, "todo"), { recursive: true }) await fs.mkdir(path.join(storageDir, "permission"), { recursive: true }) await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true }) - await fs.mkdir(path.join(storageDir, "share"), { recursive: true }) // Create legacy marker to indicate JSON storage exists await Bun.write(path.join(storageDir, "migration"), "1") return storageDir @@ -79,33 +71,31 @@ function createTestDb() { // Apply schema migrations using drizzle migrate const dir = path.join(import.meta.dirname, "../../migration") - const journal = JSON.parse(readFileSync(path.join(dir, "meta/_journal.json"), "utf-8")) as { - entries: { tag: string; when: number }[] - } - const migrations = journal.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) + const entries = readdirSync(dir, { withFileTypes: true }) + const migrations = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => ({ + sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"), + timestamp: Number(entry.name.split("_")[0]), + })) + .sort((a, b) => a.timestamp - b.timestamp) migrate(drizzle({ client: sqlite }), migrations) return sqlite } describe("JSON to SQLite migration", () => { - let tmpDir: string let storageDir: string let sqlite: Database beforeEach(async () => { - tmpDir = path.join(os.tmpdir(), "opencode-migration-test-" + Math.random().toString(36).slice(2)) - await fs.mkdir(tmpDir, { recursive: true }) - storageDir = await setupStorageDir(tmpDir) + storageDir = await setupStorageDir() sqlite = createTestDb() }) afterEach(async () => { sqlite.close() - await fs.rm(tmpDir, { recursive: true, force: true }) + await fs.rm(storageDir, { recursive: true, force: true }) }) test("migrates project", async () => { @@ -121,7 +111,7 @@ describe("JSON to SQLite migration", () => { }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.projects).toBe(1) @@ -161,7 +151,7 @@ describe("JSON to SQLite migration", () => { }), ) - await migrateFromJson(sqlite, storageDir) + await JsonMigration.run(sqlite) const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() @@ -198,7 +188,7 @@ describe("JSON to SQLite migration", () => { JSON.stringify({ ...fixtures.part }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.messages).toBe(1) expect(stats?.parts).toBe(1) @@ -227,7 +217,7 @@ describe("JSON to SQLite migration", () => { }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.sessions).toBe(0) }) @@ -243,8 +233,8 @@ describe("JSON to SQLite migration", () => { }), ) - await migrateFromJson(sqlite, storageDir) - await migrateFromJson(sqlite, storageDir) + await JsonMigration.run(sqlite) + await JsonMigration.run(sqlite) const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all()