From 1e7b4768b13d90043fc29377fdfa573b3c0373d0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 24 Jan 2026 11:50:25 -0500 Subject: [PATCH 01/17] sync --- bun.lock | 58 ++ packages/opencode/drizzle.config.ts | 7 + .../migration/0000_magical_strong_guy.sql | 87 +++ .../migration/meta/0000_snapshot.json | 587 ++++++++++++++++++ .../opencode/migration/meta/_journal.json | 13 + packages/opencode/package.json | 2 + packages/opencode/script/check-migrations.ts | 16 + .../opencode/script/generate-migrations.ts | 49 ++ packages/opencode/src/cli/cmd/database.ts | 147 +++++ packages/opencode/src/cli/cmd/import.ts | 27 +- packages/opencode/src/cli/cmd/stats.ts | 24 +- packages/opencode/src/index.ts | 2 + packages/opencode/src/permission/next.ts | 15 +- packages/opencode/src/project/project.sql.ts | 14 + packages/opencode/src/project/project.ts | 204 +++--- packages/opencode/src/server/error.ts | 4 +- packages/opencode/src/server/routes/pty.ts | 4 +- packages/opencode/src/server/server.ts | 4 +- packages/opencode/src/session/index.ts | 163 +++-- packages/opencode/src/session/message-v2.ts | 32 +- packages/opencode/src/session/revert.ts | 14 +- packages/opencode/src/session/session.sql.ts | 83 +++ packages/opencode/src/session/summary.ts | 13 +- packages/opencode/src/session/todo.ts | 19 +- packages/opencode/src/share/share-next.ts | 23 +- packages/opencode/src/share/share.sql.ts | 19 + packages/opencode/src/sql.d.ts | 4 + packages/opencode/src/storage/db.ts | 87 +++ .../opencode/src/storage/json-migration.ts | 300 +++++++++ .../src/storage/migrations.generated.ts | 6 + packages/opencode/src/storage/storage.ts | 227 ------- packages/opencode/src/util/lazy.ts | 11 +- packages/opencode/src/worktree/index.ts | 7 +- .../opencode/test/permission/next.test.ts | 1 - packages/opencode/test/preload.ts | 14 +- .../opencode/test/project/project.test.ts | 17 +- .../test/storage/json-migration.test.ts | 279 +++++++++ 37 files changed, 2151 insertions(+), 432 deletions(-) create mode 100644 packages/opencode/drizzle.config.ts create mode 100644 packages/opencode/migration/0000_magical_strong_guy.sql create mode 100644 packages/opencode/migration/meta/0000_snapshot.json create mode 100644 packages/opencode/migration/meta/_journal.json create mode 100644 packages/opencode/script/check-migrations.ts create mode 100644 packages/opencode/script/generate-migrations.ts create mode 100644 packages/opencode/src/cli/cmd/database.ts create mode 100644 packages/opencode/src/project/project.sql.ts create mode 100644 packages/opencode/src/session/session.sql.ts create mode 100644 packages/opencode/src/share/share.sql.ts create mode 100644 packages/opencode/src/sql.d.ts create mode 100644 packages/opencode/src/storage/db.ts create mode 100644 packages/opencode/src/storage/json-migration.ts create mode 100644 packages/opencode/src/storage/migrations.generated.ts delete mode 100644 packages/opencode/src/storage/storage.ts create mode 100644 packages/opencode/test/storage/json-migration.test.ts diff --git a/bun.lock b/bun.lock index 34a6488ba01..6211ff1482c 100644 --- a/bun.lock +++ b/bun.lock @@ -311,6 +311,7 @@ "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", + "drizzle-orm": "0.44.2", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -352,6 +353,7 @@ "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", + "drizzle-kit": "0.31.0", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -4414,6 +4416,10 @@ "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@0.31.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg=="], + + "opencode/drizzle-orm": ["drizzle-orm@0.44.2", "", { "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.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@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", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], + "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=="], @@ -5020,6 +5026,8 @@ "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/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=="], @@ -5192,6 +5200,56 @@ "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/packages/opencode/drizzle.config.ts b/packages/opencode/drizzle.config.ts new file mode 100644 index 00000000000..551a2384c5a --- /dev/null +++ b/packages/opencode/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/**/*.sql.ts", + out: "./migration", +}) diff --git a/packages/opencode/migration/0000_magical_strong_guy.sql b/packages/opencode/migration/0000_magical_strong_guy.sql new file mode 100644 index 00000000000..e25f0d3d56b --- /dev/null +++ b/packages/opencode/migration/0000_magical_strong_guy.sql @@ -0,0 +1,87 @@ +CREATE TABLE `project` ( + `id` text PRIMARY KEY NOT NULL, + `worktree` text NOT NULL, + `vcs` text, + `name` text, + `icon_url` text, + `icon_color` text, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `time_initialized` integer, + `sandboxes` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `message` ( + `id` text PRIMARY KEY NOT NULL, + `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 +); +--> statement-breakpoint +CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint +CREATE TABLE `part` ( + `id` text PRIMARY KEY NOT NULL, + `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 +); +--> 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, + `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 +); +--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `parent_id` text, + `slug` text NOT NULL, + `directory` text NOT NULL, + `title` text NOT NULL, + `version` text NOT NULL, + `share_url` text, + `summary_additions` integer, + `summary_deletions` integer, + `summary_files` integer, + `summary_diffs` text, + `revert_message_id` text, + `revert_part_id` text, + `revert_snapshot` text, + `revert_diff` text, + `permission` text, + `time_created` integer NOT NULL, + `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 +); +--> 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 +); +--> 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 +); +--> statement-breakpoint +CREATE TABLE `share` ( + `session_id` text PRIMARY KEY NOT NULL, + `data` text NOT NULL +); diff --git a/packages/opencode/migration/meta/0000_snapshot.json b/packages/opencode/migration/meta/0000_snapshot.json new file mode 100644 index 00000000000..efec141ea67 --- /dev/null +++ b/packages/opencode/migration/meta/0000_snapshot.json @@ -0,0 +1,587 @@ +{ + "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 new file mode 100644 index 00000000000..4ab81e184d6 --- /dev/null +++ b/packages/opencode/migration/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "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 822b581dd73..6c3f3a5cca2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", + "drizzle-kit": "0.31.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", @@ -96,6 +97,7 @@ "chokidar": "4.0.3", "clipboardy": "4.0.0", "decimal.js": "10.5.0", + "drizzle-orm": "0.44.2", "diff": "catalog:", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/script/check-migrations.ts b/packages/opencode/script/check-migrations.ts new file mode 100644 index 00000000000..f5eaf79323b --- /dev/null +++ b/packages/opencode/script/check-migrations.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env bun + +import { $ } from "bun" + +// drizzle-kit check compares schema to migrations, exits non-zero if drift +const result = await $`bun drizzle-kit check`.quiet().nothrow() + +if (result.exitCode !== 0) { + console.error("Schema has changes not captured in migrations!") + console.error("Run: bun drizzle-kit generate") + console.error("") + console.error(result.stderr.toString()) + process.exit(1) +} + +console.log("Migrations are up to date") diff --git a/packages/opencode/script/generate-migrations.ts b/packages/opencode/script/generate-migrations.ts new file mode 100644 index 00000000000..47c2e0c5e11 --- /dev/null +++ b/packages/opencode/script/generate-migrations.ts @@ -0,0 +1,49 @@ +#!/usr/bin/env bun + +import { Glob } from "bun" +import path from "path" +import fs from "fs" + +const migrationsDir = "./migration" +const outFile = "./src/storage/migrations.generated.ts" + +if (!fs.existsSync(migrationsDir)) { + console.log("No migrations directory found, creating empty migrations file") + await Bun.write( + outFile, + `// Auto-generated - do not edit +export const migrations: { name: string; sql: string }[] = [] +`, + ) + process.exit(0) +} + +const files = Array.from(new Glob("*.sql").scanSync({ cwd: migrationsDir })).sort() + +if (files.length === 0) { + console.log("No migrations found, creating empty migrations file") + await Bun.write( + outFile, + `// Auto-generated - do not edit +export const migrations: { name: string; sql: string }[] = [] +`, + ) + process.exit(0) +} + +const imports = files.map((f, i) => `import m${i} from "../../migration/${f}" with { type: "text" }`).join("\n") + +const entries = files.map((f, i) => ` { name: "${path.basename(f, ".sql")}", sql: m${i} },`).join("\n") + +await Bun.write( + outFile, + `// Auto-generated - do not edit +${imports} + +export const migrations = [ +${entries} +] +`, +) + +console.log(`Generated migrations file with ${files.length} migrations`) diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts new file mode 100644 index 00000000000..5b3c1485f30 --- /dev/null +++ b/packages/opencode/src/cli/cmd/database.ts @@ -0,0 +1,147 @@ +import type { Argv } from "yargs" +import { cmd } from "./cmd" +import { bootstrap } from "../bootstrap" +import { UI } from "../ui" +import { db } 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 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 db().select().from(SessionTable).all()) { + const dir = path.join(sessionDir, row.projectID) + 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 db().select().from(MessageTable).all()) { + const dir = path.join(messageDir, row.sessionID) + 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 db().select().from(PartTable).all()) { + const dir = path.join(partDir, row.messageID) + 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 db().select().from(SessionDiffTable).all()) { + await Bun.write(path.join(diffDir, `${row.sessionID}.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 db().select().from(TodoTable).all()) { + await Bun.write(path.join(todoDir, `${row.sessionID}.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 db().select().from(PermissionTable).all()) { + await Bun.write(path.join(permDir, `${row.projectID}.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 db().select().from(SessionShareTable).all()) { + await Bun.write(path.join(sessionShareDir, `${row.sessionID}.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 db().select().from(ShareTable).all()) { + await Bun.write(path.join(shareDir, `${row.sessionID}.json`), JSON.stringify(row.data, null, 2)) + stats.shares++ + } + + // Create migration marker so this can be imported back + await Bun.write(path.join(outDir, "migration"), Date.now().toString()) + + 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/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 9d7e8c56171..c78776b9d76 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -2,7 +2,8 @@ import type { Argv } from "yargs" import { Session } from "../../session" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" -import { Storage } from "../../storage/storage" +import { db } from "../../storage/db" +import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { Instance } from "../../project/instance" import { EOL } from "os" @@ -81,13 +82,31 @@ export const ImportCommand = cmd({ return } - await Storage.write(["session", Instance.project.id, exportData.info.id], exportData.info) + db().insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run() for (const msg of exportData.messages) { - await Storage.write(["message", exportData.info.id, msg.info.id], msg.info) + db() + .insert(MessageTable) + .values({ + id: msg.info.id, + sessionID: exportData.info.id, + createdAt: msg.info.time?.created ?? Date.now(), + data: msg.info, + }) + .onConflictDoNothing() + .run() for (const part of msg.parts) { - await Storage.write(["part", msg.info.id, part.id], part) + db() + .insert(PartTable) + .values({ + id: part.id, + messageID: msg.info.id, + sessionID: exportData.info.id, + data: part, + }) + .onConflictDoNothing() + .run() } } diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index d78c4f0abd1..21ee97fa826 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -2,7 +2,8 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { Session } from "../../session" import { bootstrap } from "../bootstrap" -import { Storage } from "../../storage/storage" +import { db } from "../../storage/db" +import { SessionTable } from "../../session/session.sql" import { Project } from "../../project/project" import { Instance } from "../../project/instance" @@ -83,25 +84,8 @@ async function getCurrentProject(): Promise { } async function getAllSessions(): Promise { - const sessions: Session.Info[] = [] - - const projectKeys = await Storage.list(["project"]) - const projects = await Promise.all(projectKeys.map((key) => Storage.read(key))) - - for (const project of projects) { - if (!project) continue - - const sessionKeys = await Storage.list(["session", project.id]) - const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read(key))) - - for (const session of projectSessions) { - if (session) { - sessions.push(session) - } - } - } - - return sessions + const rows = db().select().from(SessionTable).all() + return rows.map((row) => Session.fromRow(row)) } export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise { diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 6dc5e99e91e..e73fda21b7b 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,6 +26,7 @@ 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" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -97,6 +98,7 @@ 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/permission/next.ts b/packages/opencode/src/permission/next.ts index 2481f104ed1..b625bb57fc5 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -3,7 +3,9 @@ import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" import { Identifier } from "@/id/id" import { Instance } from "@/project/instance" -import { Storage } from "@/storage/storage" +import { db } from "@/storage/db" +import { PermissionTable } from "@/session/session.sql" +import { eq } from "drizzle-orm" import { fn } from "@/util/fn" import { Log } from "@/util/log" import { Wildcard } from "@/util/wildcard" @@ -105,9 +107,10 @@ export namespace PermissionNext { ), } - const state = Instance.state(async () => { + const state = Instance.state(() => { const projectID = Instance.project.id - const stored = await Storage.read(["permission", projectID]).catch(() => [] as Ruleset) + const row = db().select().from(PermissionTable).where(eq(PermissionTable.projectID, projectID)).get() + const stored = row?.data ?? ([] as Ruleset) const pending: Record< string, @@ -222,7 +225,8 @@ export namespace PermissionNext { // TODO: we don't save the permission ruleset to disk yet until there's // UI to manage it - // await Storage.write(["permission", Instance.project.id], s.approved) + // db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved }) + // .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run() return } }, @@ -275,6 +279,7 @@ export namespace PermissionNext { } export async function list() { - return state().then((x) => Object.values(x.pending).map((x) => x.info)) + const s = await state() + return Object.values(s.pending).map((x) => x.info) } } diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts new file mode 100644 index 00000000000..651d537cf2b --- /dev/null +++ b/packages/opencode/src/project/project.sql.ts @@ -0,0 +1,14 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" + +export const ProjectTable = sqliteTable("project", { + id: text("id").primaryKey(), + worktree: text("worktree").notNull(), + vcs: text("vcs"), + name: text("name"), + icon_url: text("icon_url"), + icon_color: text("icon_color"), + time_created: integer("time_created").notNull(), + time_updated: integer("time_updated").notNull(), + time_initialized: integer("time_initialized"), + sandboxes: text("sandboxes", { mode: "json" }).notNull().$type(), +}) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index f6902de4e1b..71c6a9bc7b0 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -3,10 +3,12 @@ import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import path from "path" import { $ } from "bun" -import { Storage } from "../storage/storage" +import { db } from "../storage/db" +import { ProjectTable } from "./project.sql" +import { SessionTable } from "../session/session.sql" +import { eq } from "drizzle-orm" import { Log } from "../util/log" import { Flag } from "@/flag/flag" -import { Session } from "../session" import { work } from "../util/queue" import { fn } from "@opencode-ai/util/fn" import { BusEvent } from "@/bus/bus-event" @@ -50,6 +52,28 @@ export namespace Project { Updated: BusEvent.define("project.updated", Info), } + type Row = typeof ProjectTable.$inferSelect + + export function fromRow(row: Row): Info { + const icon = + row.icon_url || row.icon_color + ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } + : undefined + return { + id: row.id, + worktree: row.worktree, + vcs: row.vcs as Info["vcs"], + name: row.name ?? undefined, + icon, + time: { + created: row.time_created, + updated: row.time_updated, + initialized: row.time_initialized ?? undefined, + }, + sandboxes: row.sandboxes, + } + } + export async function fromDirectory(directory: string) { log.info("fromDirectory", { directory }) @@ -175,9 +199,10 @@ export namespace Project { } }) - let existing = await Storage.read(["project", id]).catch(() => undefined) - if (!existing) { - existing = { + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, id)).get() + const existing = await iife(async () => { + if (row) return fromRow(row) + const fresh: Info = { id, worktree, vcs: vcs as Info["vcs"], @@ -190,10 +215,8 @@ export namespace Project { if (id !== "global") { await migrateFromGlobal(id, worktree) } - } - - // migrate old projects before sandboxes - if (!existing.sandboxes) existing.sandboxes = [] + return fresh + }) if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing) @@ -208,7 +231,29 @@ export namespace Project { } if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox) result.sandboxes = result.sandboxes.filter((x) => existsSync(x)) - await Storage.write(["project", id], result) + const insert = { + id: result.id, + worktree: result.worktree, + vcs: result.vcs, + name: result.name, + icon_url: result.icon?.url, + icon_color: result.icon?.color, + time_created: result.time.created, + time_updated: result.time.updated, + time_initialized: result.time.initialized, + sandboxes: result.sandboxes, + } + const updateSet = { + worktree: result.worktree, + vcs: result.vcs, + name: result.name, + icon_url: result.icon?.url, + icon_color: result.icon?.color, + time_updated: result.time.updated, + time_initialized: result.time.initialized, + sandboxes: result.sandboxes, + } + db().insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run() GlobalBus.emit("event", { payload: { type: Event.Updated.type, @@ -249,42 +294,47 @@ export namespace Project { } async function migrateFromGlobal(newProjectID: string, worktree: string) { - const globalProject = await Storage.read(["project", "global"]).catch(() => undefined) - if (!globalProject) return + const globalRow = db().select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get() + if (!globalRow) return - const globalSessions = await Storage.list(["session", "global"]).catch(() => []) + const globalSessions = db().select().from(SessionTable).where(eq(SessionTable.projectID, "global")).all() if (globalSessions.length === 0) return log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length }) - await work(10, globalSessions, async (key) => { - const sessionID = key[key.length - 1] - const session = await Storage.read(key).catch(() => undefined) - if (!session) return - if (session.directory && session.directory !== worktree) return + await work(10, globalSessions, async (row) => { + // Skip sessions that belong to a different directory + if (row.directory && row.directory !== worktree) return - session.projectID = newProjectID - log.info("migrating session", { sessionID, from: "global", to: newProjectID }) - await Storage.write(["session", newProjectID, sessionID], session) - await Storage.remove(key) + log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID }) + db().update(SessionTable).set({ projectID: newProjectID }).where(eq(SessionTable.id, row.id)).run() }).catch((error) => { log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID }) }) } - export async function setInitialized(projectID: string) { - await Storage.update(["project", projectID], (draft) => { - draft.time.initialized = Date.now() - }) + export function setInitialized(projectID: string) { + db() + .update(ProjectTable) + .set({ + time_initialized: Date.now(), + }) + .where(eq(ProjectTable.id, projectID)) + .run() + } + + export function list() { + return db() + .select() + .from(ProjectTable) + .all() + .map((row) => fromRow(row)) } - export async function list() { - const keys = await Storage.list(["project"]) - const projects = await Promise.all(keys.map((x) => Storage.read(x))) - return projects.map((project) => ({ - ...project, - sandboxes: project.sandboxes?.filter((x) => existsSync(x)), - })) + export function get(projectID: string): Info | undefined { + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + if (!row) return undefined + return fromRow(row) } export const update = fn( @@ -295,43 +345,35 @@ export namespace Project { commands: Info.shape.commands.optional(), }), async (input) => { - const result = await Storage.update(["project", input.projectID], (draft) => { - if (input.name !== undefined) draft.name = input.name - if (input.icon !== undefined) { - draft.icon = { - ...draft.icon, - } - if (input.icon.url !== undefined) draft.icon.url = input.icon.url - if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined - if (input.icon.color !== undefined) draft.icon.color = input.icon.color - } - - if (input.commands?.start !== undefined) { - const start = input.commands.start || undefined - draft.commands = { - ...(draft.commands ?? {}), - } - draft.commands.start = start - if (!draft.commands.start) draft.commands = undefined - } - - draft.time.updated = Date.now() - }) + const result = db() + .update(ProjectTable) + .set({ + name: input.name, + icon_url: input.icon?.url, + icon_color: input.icon?.color, + time_updated: Date.now(), + }) + .where(eq(ProjectTable.id, input.projectID)) + .returning() + .get() + if (!result) throw new Error(`Project not found: ${input.projectID}`) + const data = fromRow(result) GlobalBus.emit("event", { payload: { type: Event.Updated.type, - properties: result, + properties: data, }, }) - return result + return data }, ) export async function sandboxes(projectID: string) { - const project = await Storage.read(["project", projectID]).catch(() => undefined) - if (!project?.sandboxes) return [] + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + if (!row) return [] + const data = fromRow(row) const valid: string[] = [] - for (const dir of project.sandboxes) { + for (const dir of data.sandboxes) { const stat = await fs.stat(dir).catch(() => undefined) if (stat?.isDirectory()) valid.push(dir) } @@ -339,33 +381,45 @@ export namespace Project { } export async function addSandbox(projectID: string, directory: string) { - const result = await Storage.update(["project", projectID], (draft) => { - const sandboxes = draft.sandboxes ?? [] - if (!sandboxes.includes(directory)) sandboxes.push(directory) - draft.sandboxes = sandboxes - draft.time.updated = Date.now() - }) + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + if (!row) throw new Error(`Project not found: ${projectID}`) + const sandboxes = [...row.sandboxes] + if (!sandboxes.includes(directory)) sandboxes.push(directory) + const result = db() + .update(ProjectTable) + .set({ sandboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, projectID)) + .returning() + .get() + if (!result) throw new Error(`Project not found: ${projectID}`) + const data = fromRow(result) GlobalBus.emit("event", { payload: { type: Event.Updated.type, - properties: result, + properties: data, }, }) - return result + return data } export async function removeSandbox(projectID: string, directory: string) { - const result = await Storage.update(["project", projectID], (draft) => { - const sandboxes = draft.sandboxes ?? [] - draft.sandboxes = sandboxes.filter((sandbox) => sandbox !== directory) - draft.time.updated = Date.now() - }) + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + if (!row) throw new Error(`Project not found: ${projectID}`) + const sandboxes = row.sandboxes.filter((s: string) => s !== directory) + const result = db() + .update(ProjectTable) + .set({ sandboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, projectID)) + .returning() + .get() + if (!result) throw new Error(`Project not found: ${projectID}`) + const data = fromRow(result) GlobalBus.emit("event", { payload: { type: Event.Updated.type, - properties: result, + properties: data, }, }) - return result + return data } } diff --git a/packages/opencode/src/server/error.ts b/packages/opencode/src/server/error.ts index 26e2dfcb121..cc5fa961877 100644 --- a/packages/opencode/src/server/error.ts +++ b/packages/opencode/src/server/error.ts @@ -1,6 +1,6 @@ import { resolver } from "hono-openapi" import z from "zod" -import { Storage } from "../storage/storage" +import { NotFoundError } from "../storage/db" export const ERRORS = { 400: { @@ -25,7 +25,7 @@ export const ERRORS = { description: "Not found", content: { "application/json": { - schema: resolver(Storage.NotFoundError.Schema), + schema: resolver(NotFoundError.Schema), }, }, }, diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts index 1ac6cf79715..d1f3820cb8f 100644 --- a/packages/opencode/src/server/routes/pty.ts +++ b/packages/opencode/src/server/routes/pty.ts @@ -3,7 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi" import { upgradeWebSocket } from "hono/bun" import z from "zod" import { Pty } from "@/pty" -import { Storage } from "../../storage/storage" +import { NotFoundError } from "../../storage/db" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -76,7 +76,7 @@ export const PtyRoutes = lazy(() => async (c) => { const info = Pty.get(c.req.valid("param").ptyID) if (!info) { - throw new Storage.NotFoundError({ message: "Session not found" }) + throw new NotFoundError({ message: "Session not found" }) } return c.json(info) }, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index fa646f21ea8..0ffb10e0b66 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -31,7 +31,7 @@ import { ExperimentalRoutes } from "./routes/experimental" import { ProviderRoutes } from "./routes/provider" import { lazy } from "../util/lazy" import { InstanceBootstrap } from "../project/bootstrap" -import { Storage } from "../storage/storage" +import { NotFoundError } from "../storage/db" import type { ContentfulStatusCode } from "hono/utils/http-status" import { websocket } from "hono/bun" import { HTTPException } from "hono/http-exception" @@ -65,7 +65,7 @@ export namespace Server { }) if (err instanceof NamedError) { let status: ContentfulStatusCode - if (err instanceof Storage.NotFoundError) status = 404 + if (err instanceof NotFoundError) status = 404 else if (err instanceof Provider.ModelNotFoundError) status = 400 else if (err.name.startsWith("Worktree")) status = 400 else status = 500 diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index b81a21a57be..84b567b9425 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,7 +10,10 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { Storage } from "../storage/storage" +import { db, NotFoundError } from "../storage/db" +import { SessionTable, MessageTable, PartTable, SessionDiffTable } from "./session.sql" +import { ShareTable } from "../share/share.sql" +import { eq } from "drizzle-orm" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" @@ -39,6 +42,75 @@ export namespace Session { ).test(title) } + type SessionRow = typeof SessionTable.$inferSelect + + export function fromRow(row: SessionRow): Info { + const summary = + row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null + ? { + additions: row.summary_additions ?? 0, + deletions: row.summary_deletions ?? 0, + files: row.summary_files ?? 0, + diffs: row.summary_diffs ?? undefined, + } + : undefined + const share = row.share_url ? { url: row.share_url } : undefined + const revert = + row.revert_messageID !== null + ? { + messageID: row.revert_messageID, + partID: row.revert_partID ?? undefined, + snapshot: row.revert_snapshot ?? undefined, + diff: row.revert_diff ?? undefined, + } + : undefined + return { + id: row.id, + slug: row.slug, + projectID: row.projectID, + directory: row.directory, + parentID: row.parentID ?? undefined, + title: row.title, + version: row.version, + summary, + share, + revert, + permission: row.permission ?? undefined, + time: { + created: row.time_created, + updated: row.time_updated, + compacting: row.time_compacting ?? undefined, + archived: row.time_archived ?? undefined, + }, + } + } + + export function toRow(info: Info) { + return { + id: info.id, + projectID: info.projectID, + parentID: info.parentID, + slug: info.slug, + directory: info.directory, + title: info.title, + version: info.version, + share_url: info.share?.url, + summary_additions: info.summary?.additions, + summary_deletions: info.summary?.deletions, + summary_files: info.summary?.files, + summary_diffs: info.summary?.diffs, + revert_messageID: info.revert?.messageID ?? null, + revert_partID: info.revert?.partID ?? null, + revert_snapshot: info.revert?.snapshot ?? null, + revert_diff: info.revert?.diff ?? null, + permission: info.permission, + time_created: info.time.created, + time_updated: info.time.updated, + time_compacting: info.time.compacting, + time_archived: info.time.archived, + } + } + export const Info = z .object({ id: Identifier.schema("session"), @@ -211,7 +283,7 @@ export namespace Session { }, } log.info("created", result) - await Storage.write(["session", Instance.project.id, result.id], result) + db().insert(SessionTable).values(toRow(result)).run() Bus.publish(Event.Created, { info: result, }) @@ -240,12 +312,14 @@ export namespace Session { } export const get = fn(Identifier.schema("session"), async (id) => { - const read = await Storage.read(["session", Instance.project.id, id]) - return read as Info + const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get() + if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + return fromRow(row) }) export const getShare = fn(Identifier.schema("session"), async (id) => { - return Storage.read(["share", id]) + const row = db().select().from(ShareTable).where(eq(ShareTable.sessionID, id)).get() + return row?.data }) export const share = fn(Identifier.schema("session"), async (id) => { @@ -280,23 +354,24 @@ export namespace Session { ) }) - export async function update(id: string, editor: (session: Info) => void, options?: { touch?: boolean }) { - const project = Instance.project - const result = await Storage.update(["session", project.id, id], (draft) => { - editor(draft) - if (options?.touch !== false) { - draft.time.updated = Date.now() - } - }) + export function update(id: string, editor: (session: Info) => void, options?: { touch?: boolean }) { + const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get() + if (!row) throw new Error(`Session not found: ${id}`) + const data = fromRow(row) + editor(data) + if (options?.touch !== false) { + data.time.updated = Date.now() + } + db().update(SessionTable).set(toRow(data)).where(eq(SessionTable.id, id)).run() Bus.publish(Event.Updated, { - info: result, + info: data, }) - return result + return data } export const diff = fn(Identifier.schema("session"), async (sessionID) => { - const diffs = await Storage.read(["session_diff", sessionID]) - return diffs ?? [] + const row = db().select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, sessionID)).get() + return row?.data ?? [] }) export const messages = fn( @@ -315,22 +390,17 @@ export namespace Session { }, ) - export async function* list() { + export function* list() { const project = Instance.project - for (const item of await Storage.list(["session", project.id])) { - yield Storage.read(item) + const rows = db().select().from(SessionTable).where(eq(SessionTable.projectID, project.id)).all() + for (const row of rows) { + yield fromRow(row) } } export const children = fn(Identifier.schema("session"), async (parentID) => { - const project = Instance.project - const result = [] as Session.Info[] - for (const item of await Storage.list(["session", project.id])) { - const session = await Storage.read(item) - if (session.parentID !== parentID) continue - result.push(session) - } - return result + const rows = db().select().from(SessionTable).where(eq(SessionTable.parentID, parentID)).all() + return rows.map((row) => fromRow(row)) }) export const remove = fn(Identifier.schema("session"), async (sessionID) => { @@ -341,13 +411,8 @@ export namespace Session { await remove(child.id) } await unshare(sessionID).catch(() => {}) - for (const msg of await Storage.list(["message", sessionID])) { - for (const part of await Storage.list(["part", msg.at(-1)!])) { - await Storage.remove(part) - } - await Storage.remove(msg) - } - await Storage.remove(["session", project.id, sessionID]) + // CASCADE delete handles messages and parts automatically + db().delete(SessionTable).where(eq(SessionTable.id, sessionID)).run() Bus.publish(Event.Deleted, { info: session, }) @@ -357,7 +422,17 @@ export namespace Session { }) export const updateMessage = fn(MessageV2.Info, async (msg) => { - await Storage.write(["message", msg.sessionID, msg.id], msg) + const createdAt = msg.role === "user" ? msg.time.created : msg.time.created + db() + .insert(MessageTable) + .values({ + id: msg.id, + sessionID: msg.sessionID, + createdAt, + data: msg, + }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) + .run() Bus.publish(MessageV2.Event.Updated, { info: msg, }) @@ -370,7 +445,8 @@ export namespace Session { messageID: Identifier.schema("message"), }), async (input) => { - await Storage.remove(["message", input.sessionID, input.messageID]) + // CASCADE delete handles parts automatically + db().delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run() Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: input.messageID, @@ -386,7 +462,7 @@ export namespace Session { partID: Identifier.schema("part"), }), async (input) => { - await Storage.remove(["part", input.messageID, input.partID]) + db().delete(PartTable).where(eq(PartTable.id, input.partID)).run() Bus.publish(MessageV2.Event.PartRemoved, { sessionID: input.sessionID, messageID: input.messageID, @@ -411,7 +487,16 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (input) => { const part = "delta" in input ? input.part : input const delta = "delta" in input ? input.delta : undefined - await Storage.write(["part", part.messageID, part.id], part) + db() + .insert(PartTable) + .values({ + id: part.id, + messageID: part.messageID, + sessionID: part.sessionID, + data: part, + }) + .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) + .run() Bus.publish(MessageV2.Event.PartUpdated, { part, delta, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 83ca72addb1..2dab09918ae 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -6,7 +6,9 @@ import { Identifier } from "../id/id" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" import { fn } from "@/util/fn" -import { Storage } from "@/storage/storage" +import { db } from "@/storage/db" +import { MessageTable, PartTable } from "./session.sql" +import { eq, desc } from "drizzle-orm" import { ProviderTransform } from "@/provider/transform" import { STATUS_CODES } from "http" import { iife } from "@/util/iife" @@ -607,21 +609,23 @@ export namespace MessageV2 { } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { - const list = await Array.fromAsync(await Storage.list(["message", sessionID])) - for (let i = list.length - 1; i >= 0; i--) { - yield await get({ - sessionID, - messageID: list[i][2], - }) + const rows = db() + .select() + .from(MessageTable) + .where(eq(MessageTable.sessionID, sessionID)) + .orderBy(desc(MessageTable.createdAt)) + .all() + for (const row of rows) { + yield { + info: row.data, + parts: await parts(row.id), + } } }) export const parts = fn(Identifier.schema("message"), async (messageID) => { - const result = [] as MessageV2.Part[] - for (const item of await Storage.list(["part", messageID])) { - const read = await Storage.read(item) - result.push(read) - } + const rows = db().select().from(PartTable).where(eq(PartTable.messageID, messageID)).all() + const result = rows.map((row) => row.data) result.sort((a, b) => (a.id > b.id ? 1 : -1)) return result }) @@ -632,8 +636,10 @@ export namespace MessageV2 { messageID: Identifier.schema("message"), }), async (input) => { + const row = db().select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get() + if (!row) throw new Error(`Message not found: ${input.messageID}`) return { - info: await Storage.read(["message", input.sessionID, input.messageID]), + info: row.data, parts: await parts(input.messageID), } }, diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 7afe44e2ce3..fb6e0a5ec39 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -5,7 +5,9 @@ import { MessageV2 } from "./message-v2" import { Session } from "." import { Log } from "../util/log" import { splitWhen } from "remeda" -import { Storage } from "../storage/storage" +import { db } from "../storage/db" +import { SessionDiffTable, MessageTable, PartTable } from "./session.sql" +import { eq } from "drizzle-orm" import { Bus } from "../bus" import { SessionPrompt } from "./prompt" import { SessionSummary } from "./summary" @@ -60,7 +62,11 @@ 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 }) - await Storage.write(["session_diff", input.sessionID], diffs) + db() + .insert(SessionDiffTable) + .values({ sessionID: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .run() Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -97,7 +103,7 @@ export namespace SessionRevert { const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID) msgs = preserve for (const msg of remove) { - await Storage.remove(["message", sessionID, msg.info.id]) + db().delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run() await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id }) } const last = preserve.at(-1) @@ -106,7 +112,7 @@ export namespace SessionRevert { const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID) last.parts = preserveParts for (const part of removeParts) { - await Storage.remove(["part", last.info.id, part.id]) + db().delete(PartTable).where(eq(PartTable.id, part.id)).run() await Bus.publish(MessageV2.Event.PartRemoved, { sessionID: sessionID, messageID: last.info.id, diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts new file mode 100644 index 00000000000..be35dd17033 --- /dev/null +++ b/packages/opencode/src/session/session.sql.ts @@ -0,0 +1,83 @@ +import { sqliteTable, text, integer, index } 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( + "session", + { + id: text("id").primaryKey(), + projectID: text("project_id") + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + parentID: text("parent_id"), + slug: text("slug").notNull(), + directory: text("directory").notNull(), + title: text("title").notNull(), + version: text("version").notNull(), + share_url: text("share_url"), + summary_additions: integer("summary_additions"), + summary_deletions: integer("summary_deletions"), + summary_files: integer("summary_files"), + summary_diffs: text("summary_diffs", { mode: "json" }).$type(), + revert_messageID: text("revert_message_id"), + revert_partID: text("revert_part_id"), + revert_snapshot: text("revert_snapshot"), + revert_diff: text("revert_diff"), + permission: text("permission", { mode: "json" }).$type(), + time_created: integer("time_created").notNull(), + time_updated: integer("time_updated").notNull(), + time_compacting: integer("time_compacting"), + time_archived: integer("time_archived"), + }, + (table) => [index("session_project_idx").on(table.projectID), index("session_parent_idx").on(table.parentID)], +) + +export const MessageTable = sqliteTable( + "message", + { + id: text("id").primaryKey(), + sessionID: text("session_id") + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + createdAt: integer("created_at").notNull(), + data: text("data", { mode: "json" }).notNull().$type(), + }, + (table) => [index("message_session_idx").on(table.sessionID)], +) + +export const PartTable = sqliteTable( + "part", + { + id: text("id").primaryKey(), + messageID: text("message_id") + .notNull() + .references(() => MessageTable.id, { onDelete: "cascade" }), + sessionID: text("session_id").notNull(), + data: text("data", { mode: "json" }).notNull().$type(), + }, + (table) => [index("part_message_idx").on(table.messageID), index("part_session_idx").on(table.sessionID)], +) + +export const SessionDiffTable = sqliteTable("session_diff", { + sessionID: text("session_id") + .primaryKey() + .references(() => SessionTable.id, { onDelete: "cascade" }), + data: text("data", { mode: "json" }).notNull().$type(), +}) + +export const TodoTable = sqliteTable("todo", { + sessionID: text("session_id") + .primaryKey() + .references(() => SessionTable.id, { onDelete: "cascade" }), + data: text("data", { mode: "json" }).notNull().$type(), +}) + +export const PermissionTable = sqliteTable("permission", { + projectID: text("project_id") + .primaryKey() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + data: text("data", { mode: "json" }).notNull().$type(), +}) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 611d5f1c62d..a79850046d9 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -11,7 +11,9 @@ import { Snapshot } from "@/snapshot" import { Log } from "@/util/log" import path from "path" import { Instance } from "@/project/instance" -import { Storage } from "@/storage/storage" +import { db } from "@/storage/db" +import { SessionDiffTable } from "./session.sql" +import { eq } from "drizzle-orm" import { Bus } from "@/bus" import { LLM } from "./llm" @@ -54,7 +56,11 @@ export namespace SessionSummary { files: diffs.length, } }) - await Storage.write(["session_diff", input.sessionID], diffs) + db() + .insert(SessionDiffTable) + .values({ sessionID: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .run() Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -116,7 +122,8 @@ export namespace SessionSummary { messageID: Identifier.schema("message").optional(), }), async (input) => { - return Storage.read(["session_diff", input.sessionID]).catch(() => []) + const row = db().select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, input.sessionID)).get() + return row?.data ?? [] }, ) diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index aa7df7e981a..3280744662d 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,7 +1,9 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" -import { Storage } from "../storage/storage" +import { db } from "../storage/db" +import { TodoTable } from "./session.sql" +import { eq } from "drizzle-orm" export namespace Todo { export const Info = z @@ -24,14 +26,17 @@ export namespace Todo { ), } - export async function update(input: { sessionID: string; todos: Info[] }) { - await Storage.write(["todo", input.sessionID], input.todos) + export function update(input: { sessionID: string; todos: Info[] }) { + db() + .insert(TodoTable) + .values({ sessionID: input.sessionID, data: input.todos }) + .onConflictDoUpdate({ target: TodoTable.sessionID, set: { data: input.todos } }) + .run() Bus.publish(Event.Updated, input) } - export async function get(sessionID: string) { - return Storage.read(["todo", sessionID]) - .then((x) => x || []) - .catch(() => []) + export function get(sessionID: string) { + const row = db().select().from(TodoTable).where(eq(TodoTable.sessionID, sessionID)).get() + return row?.data ?? [] } } diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index dddce95cb4f..2d168204592 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -4,7 +4,9 @@ import { ulid } from "ulid" import { Provider } from "@/provider/provider" import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" -import { Storage } from "@/storage/storage" +import { db } from "@/storage/db" +import { SessionShareTable } from "./share.sql" +import { eq } from "drizzle-orm" import { Log } from "@/util/log" import type * as SDK from "@opencode-ai/sdk/v2" @@ -77,17 +79,18 @@ export namespace ShareNext { }) .then((x) => x.json()) .then((x) => x as { id: string; url: string; secret: string }) - await Storage.write(["session_share", sessionID], result) + db() + .insert(SessionShareTable) + .values({ sessionID, data: result }) + .onConflictDoUpdate({ target: SessionShareTable.sessionID, set: { data: result } }) + .run() fullSync(sessionID) return result } function get(sessionID: string) { - return Storage.read<{ - id: string - secret: string - url: string - }>(["session_share", sessionID]) + const row = db().select().from(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).get() + return row?.data } type Data = @@ -132,7 +135,7 @@ export namespace ShareNext { const queued = queue.get(sessionID) if (!queued) return queue.delete(sessionID) - const share = await get(sessionID).catch(() => undefined) + const share = get(sessionID) if (!share) return await fetch(`${await url()}/api/share/${share.id}/sync`, { @@ -152,7 +155,7 @@ export namespace ShareNext { export async function remove(sessionID: string) { if (disabled) return log.info("removing share", { sessionID }) - const share = await get(sessionID) + const share = get(sessionID) if (!share) return await fetch(`${await url()}/api/share/${share.id}`, { method: "DELETE", @@ -163,7 +166,7 @@ export namespace ShareNext { secret: share.secret, }), }) - await Storage.remove(["session_share", sessionID]) + db().delete(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).run() } async function fullSync(sessionID: string) { diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts new file mode 100644 index 00000000000..7a65fd764bd --- /dev/null +++ b/packages/opencode/src/share/share.sql.ts @@ -0,0 +1,19 @@ +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", { + sessionID: text("session_id") + .primaryKey() + .references(() => SessionTable.id, { onDelete: "cascade" }), + data: text("data", { mode: "json" }).notNull().$type<{ + id: string + secret: string + url: string + }>(), +}) + +export const ShareTable = sqliteTable("share", { + sessionID: text("session_id").primaryKey(), + data: text("data", { mode: "json" }).notNull().$type(), +}) diff --git a/packages/opencode/src/sql.d.ts b/packages/opencode/src/sql.d.ts new file mode 100644 index 00000000000..28cb1e26492 --- /dev/null +++ b/packages/opencode/src/sql.d.ts @@ -0,0 +1,4 @@ +declare module "*.sql" { + const content: string + export default content +} diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts new file mode 100644 index 00000000000..3c1a159305c --- /dev/null +++ b/packages/opencode/src/storage/db.ts @@ -0,0 +1,87 @@ +import { Database } from "bun:sqlite" +import { drizzle } from "drizzle-orm/bun-sqlite" +import { lazy } from "../util/lazy" +import { Global } from "../global" +import { Log } from "../util/log" +import { migrations } from "./migrations.generated" +import { migrateFromJson } from "./json-migration" +import { NamedError } from "@opencode-ai/util/error" +import z from "zod" +import path from "path" + +export const NotFoundError = NamedError.create( + "NotFoundError", + z.object({ + message: z.string(), + }), +) + +const log = Log.create({ service: "db" }) + +export type DB = ReturnType + +const connection = lazy(() => { + const dbPath = path.join(Global.Path.data, "opencode.db") + log.info("opening database", { path: dbPath }) + + const sqlite = new Database(dbPath, { create: true }) + + sqlite.run("PRAGMA journal_mode = WAL") + sqlite.run("PRAGMA synchronous = NORMAL") + sqlite.run("PRAGMA busy_timeout = 5000") + sqlite.run("PRAGMA cache_size = -64000") + sqlite.run("PRAGMA foreign_keys = ON") + + migrate(sqlite) + + // Run JSON migration asynchronously after schema is ready + migrateFromJson(sqlite).catch((e) => log.error("json migration failed", { error: e })) + + return drizzle(sqlite) +}) + +function migrate(sqlite: Database) { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS _migrations ( + name TEXT PRIMARY KEY, + applied_at INTEGER NOT NULL + ) + `) + + const applied = new Set( + sqlite + .query<{ name: string }, []>("SELECT name FROM _migrations") + .all() + .map((r) => r.name), + ) + + for (const migration of migrations) { + if (applied.has(migration.name)) continue + log.info("applying migration", { name: migration.name }) + + // Split by statement breakpoint and execute each statement + // Use IF NOT EXISTS variants to handle partial migrations + const statements = migration.sql.split("--> statement-breakpoint") + for (const stmt of statements) { + const trimmed = stmt.trim() + if (!trimmed) continue + + try { + sqlite.exec(trimmed) + } catch (e: any) { + // Ignore "already exists" errors for idempotency + if (e?.message?.includes("already exists")) { + log.info("skipping existing object", { statement: trimmed.slice(0, 50) }) + continue + } + throw e + } + } + + sqlite.run("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)", [migration.name, Date.now()]) + } +} + +export function db() { + return connection() +} diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts new file mode 100644 index 00000000000..96f3714c2ac --- /dev/null +++ b/packages/opencode/src/storage/json-migration.ts @@ -0,0 +1,300 @@ +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 path from "path" + +const log = Log.create({ service: "json-migration" }) + +export async function migrateFromJson(sqlite: Database, customStorageDir?: string) { + const storageDir = customStorageDir ?? path.join(Global.Path.data, "storage") + const migrationMarker = path.join(storageDir, "sqlite-migrated") + + if (await Bun.file(migrationMarker).exists()) { + log.info("json migration already completed") + return + } + + if (!(await Bun.file(path.join(storageDir, "migration")).exists())) { + log.info("no json storage found, skipping migration") + await Bun.write(migrationMarker, Date.now().toString()) + return + } + + log.info("starting json to sqlite migration", { storageDir }) + + const db = drizzle(sqlite) + const stats = { + projects: 0, + sessions: 0, + messages: 0, + parts: 0, + diffs: 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 + } + db.insert(ProjectTable) + .values({ + id: data.id, + worktree: data.worktree ?? "/", + vcs: data.vcs, + name: data.name ?? undefined, + icon_url: data.icon?.url, + icon_color: data.icon?.color, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + time_initialized: data.time?.initialized, + sandboxes: data.sandboxes ?? [], + }) + .onConflictDoNothing() + .run() + stats.projects++ + } catch (e) { + stats.errors.push(`failed to migrate project ${file}: ${e}`) + } + } + 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({ + id: data.id, + projectID: data.projectID, + parentID: data.parentID ?? null, + slug: data.slug ?? "", + directory: data.directory ?? "", + title: data.title ?? "", + version: data.version ?? "", + share_url: data.share?.url ?? null, + summary_additions: data.summary?.additions ?? null, + summary_deletions: data.summary?.deletions ?? null, + summary_files: data.summary?.files ?? null, + summary_diffs: data.summary?.diffs ?? null, + revert_messageID: data.revert?.messageID ?? null, + revert_partID: data.revert?.partID ?? null, + revert_snapshot: data.revert?.snapshot ?? null, + revert_diff: data.revert?.diff ?? null, + permission: data.permission ?? null, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + 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 + } + db.insert(MessageTable) + .values({ + id: data.id, + sessionID: data.sessionID, + createdAt: 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 }) + + // 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 + } + // 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 + } + db.insert(PartTable) + .values({ + id: data.id, + messageID: data.messageID, + sessionID: 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 }) + + // 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 + } + db.insert(SessionDiffTable).values({ 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 + } + db.insert(TodoTable).values({ sessionID, data }).onConflictDoNothing().run() + stats.todos++ + } catch (e) { + stats.errors.push(`failed to migrate todo ${file}: ${e}`) + } + } + log.info("migrated todos", { count: stats.todos }) + + // 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 + } + db.insert(PermissionTable).values({ 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 + } + db.insert(SessionShareTable).values({ 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 }) + + // 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({ sessionID, data }).onConflictDoNothing().run() + } catch (e) { + stats.errors.push(`failed to migrate share ${file}: ${e}`) + } + } + + // Mark migration complete + await Bun.write(migrationMarker, Date.now().toString()) + + 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/migrations.generated.ts b/packages/opencode/src/storage/migrations.generated.ts new file mode 100644 index 00000000000..a048c61efb2 --- /dev/null +++ b/packages/opencode/src/storage/migrations.generated.ts @@ -0,0 +1,6 @@ +// Auto-generated - do not edit +import m0 from "../../migration/0000_magical_strong_guy.sql" with { type: "text" } + +export const migrations = [ + { name: "0000_magical_strong_guy", sql: m0 }, +] diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts deleted file mode 100644 index 18f2d67e7ac..00000000000 --- a/packages/opencode/src/storage/storage.ts +++ /dev/null @@ -1,227 +0,0 @@ -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/src/util/lazy.ts b/packages/opencode/src/util/lazy.ts index 0cc6d8d5c45..55643dc6a72 100644 --- a/packages/opencode/src/util/lazy.ts +++ b/packages/opencode/src/util/lazy.ts @@ -4,9 +4,14 @@ export function lazy(fn: () => T) { const result = (): T => { if (loaded) return value as T - loaded = true - value = fn() - return value as T + try { + value = fn() + loaded = true + return value as T + } catch (e) { + // Don't mark as loaded if initialization failed + throw e + } } result.reset = () => { diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 97fe2c4fc0d..30443d36b1b 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -7,7 +7,9 @@ import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project/project" -import { Storage } from "../storage/storage" +import { db } from "../storage/db" +import { ProjectTable } from "../project/project.sql" +import { eq } from "drizzle-orm" import { fn } from "../util/fn" import { Log } from "../util/log" import { BusEvent } from "@/bus/bus-event" @@ -318,7 +320,8 @@ export namespace Worktree { }, }) - const project = await Storage.read(["project", projectID]).catch(() => undefined) + const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const project = row ? Project.fromRow(row) : undefined const startup = project?.commands?.start?.trim() ?? "" const run = async (cmd: string, kind: "project" | "worktree") => { diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 29f1efa4019..add3332048c 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -2,7 +2,6 @@ import { test, expect } from "bun:test" import os from "os" import { PermissionNext } from "../../src/permission/next" import { Instance } from "../../src/project/instance" -import { Storage } from "../../src/storage/storage" import { tmpdir } from "../fixture/fixture" // fromConfig tests diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 819166c94c6..f40e4b1e9a9 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -5,23 +5,27 @@ import path from "path" import fs from "fs/promises" import fsSync from "fs" import { afterAll } from "bun:test" -const { Global } = await import("../src/global") +// Set XDG env vars FIRST, before any src/ imports const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) await fs.mkdir(dir, { recursive: true }) afterAll(() => { fsSync.rmSync(dir, { recursive: true, force: true }) }) + +process.env["XDG_DATA_HOME"] = path.join(dir, "share") +process.env["XDG_CACHE_HOME"] = path.join(dir, "cache") +process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") +process.env["XDG_STATE_HOME"] = path.join(dir, "state") + // Set test home directory to isolate tests from user's actual home directory // This prevents tests from picking up real user configs/skills from ~/.claude/skills const testHome = path.join(dir, "home") await fs.mkdir(testHome, { recursive: true }) process.env["OPENCODE_TEST_HOME"] = testHome -process.env["XDG_DATA_HOME"] = path.join(dir, "share") -process.env["XDG_CACHE_HOME"] = path.join(dir, "cache") -process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") -process.env["XDG_STATE_HOME"] = path.join(dir, "state") +// Now safe to import Global (after XDG vars are set) +const { Global } = await import("../src/global") // Pre-fetch models.json so tests don't need the macro fallback // Also write the cache version file to prevent global/index.ts from clearing the cache diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index d44e606746e..bee8b77dd18 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test" import { Project } from "../../src/project/project" import { Log } from "../../src/util/log" -import { Storage } from "../../src/storage/storage" import { $ } from "bun" import path from "path" import { tmpdir } from "../fixture/fixture" @@ -99,11 +98,12 @@ describe("Project.discover", () => { await Project.discover(project) - const updated = await Storage.read(["project", project.id]) - expect(updated.icon).toBeDefined() - expect(updated.icon?.url).toStartWith("data:") - expect(updated.icon?.url).toContain("base64") - expect(updated.icon?.color).toBeUndefined() + const updated = Project.get(project.id) + expect(updated).toBeDefined() + expect(updated!.icon).toBeDefined() + expect(updated!.icon?.url).toStartWith("data:") + expect(updated!.icon?.url).toContain("base64") + expect(updated!.icon?.color).toBeUndefined() }) test("should not discover non-image files", async () => { @@ -114,7 +114,8 @@ describe("Project.discover", () => { await Project.discover(project) - const updated = await Storage.read(["project", project.id]) - expect(updated.icon).toBeUndefined() + const updated = Project.get(project.id) + expect(updated).toBeDefined() + expect(updated!.icon).toBeUndefined() }) }) diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts new file mode 100644 index 00000000000..c1038ab3113 --- /dev/null +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -0,0 +1,279 @@ +import { describe, test, expect, beforeEach, afterEach } from "bun:test" +import { Database } from "bun:sqlite" +import { drizzle } from "drizzle-orm/bun-sqlite" +import { eq } from "drizzle-orm" +import path from "path" +import fs from "fs/promises" +import os from "os" +import { migrateFromJson } from "../../src/storage/json-migration" +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 { migrations } from "../../src/storage/migrations.generated" + +// Test fixtures +const fixtures = { + project: { + id: "proj_test123abc", + name: "Test Project", + worktree: "/test/path", + vcs: "git" as const, + sandboxes: [], + }, + session: { + id: "ses_test456def", + projectID: "proj_test123abc", + slug: "test-session", + directory: "/test/path", + title: "Test Session", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + }, + message: { + id: "msg_test789ghi", + sessionID: "ses_test456def", + role: "user" as const, + agent: "default", + model: { providerID: "openai", modelID: "gpt-4" }, + time: { created: 1700000000000 }, + }, + part: { + id: "prt_testabc123", + messageID: "msg_test789ghi", + sessionID: "ses_test456def", + type: "text" as const, + text: "Hello, world!", + }, +} + +// Helper to create test storage directory structure +async function setupStorageDir(baseDir: string) { + const storageDir = path.join(baseDir, "storage") + 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 }) + await fs.mkdir(path.join(storageDir, "part", "msg_test789ghi"), { recursive: true }) + await fs.mkdir(path.join(storageDir, "session_diff"), { recursive: true }) + 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 +} + +// Helper to create in-memory test database with schema +function createTestDb() { + const sqlite = new Database(":memory:") + sqlite.exec("PRAGMA foreign_keys = ON") + + // Apply schema migrations + for (const migration of migrations) { + const statements = migration.sql.split("--> statement-breakpoint") + for (const stmt of statements) { + const trimmed = stmt.trim() + if (trimmed) sqlite.exec(trimmed) + } + } + + 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) + sqlite = createTestDb() + }) + + afterEach(async () => { + sqlite.close() + await fs.rm(tmpDir, { recursive: true, force: true }) + }) + + test("migrates project", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/test/path", + vcs: "git", + name: "Test Project", + time: { created: 1700000000000, updated: 1700000001000 }, + sandboxes: ["/test/sandbox"], + }), + ) + + const stats = await migrateFromJson(sqlite, storageDir) + + expect(stats?.projects).toBe(1) + + const db = drizzle(sqlite) + const projects = db.select().from(ProjectTable).all() + expect(projects.length).toBe(1) + expect(projects[0].id).toBe("proj_test123abc") + expect(projects[0].worktree).toBe("/test/path") + expect(projects[0].name).toBe("Test Project") + expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) + }) + + test("migrates session with individual columns", async () => { + // First create the project + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/test/path", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ + id: "ses_test456def", + projectID: "proj_test123abc", + slug: "test-session", + directory: "/test/dir", + title: "Test Session Title", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + summary: { additions: 10, deletions: 5, files: 3 }, + share: { url: "https://example.com/share" }, + }), + ) + + await migrateFromJson(sqlite, storageDir) + + const db = drizzle(sqlite) + const sessions = db.select().from(SessionTable).all() + expect(sessions.length).toBe(1) + expect(sessions[0].id).toBe("ses_test456def") + expect(sessions[0].projectID).toBe("proj_test123abc") + expect(sessions[0].slug).toBe("test-session") + expect(sessions[0].title).toBe("Test Session Title") + expect(sessions[0].summary_additions).toBe(10) + expect(sessions[0].summary_deletions).toBe(5) + expect(sessions[0].share_url).toBe("https://example.com/share") + }) + + test("migrates messages and parts", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + await Bun.write( + path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"), + JSON.stringify({ ...fixtures.message }), + ) + await Bun.write( + path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"), + JSON.stringify({ ...fixtures.part }), + ) + + const stats = await migrateFromJson(sqlite, storageDir) + + expect(stats?.messages).toBe(1) + expect(stats?.parts).toBe(1) + + const db = drizzle(sqlite) + const messages = db.select().from(MessageTable).all() + expect(messages.length).toBe(1) + expect(messages[0].data.id).toBe("msg_test789ghi") + + const parts = db.select().from(PartTable).all() + expect(parts.length).toBe(1) + expect(parts[0].data.id).toBe("prt_testabc123") + }) + + test("skips orphaned sessions (no parent project)", async () => { + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"), + JSON.stringify({ + id: "ses_orphan", + projectID: "proj_nonexistent", + slug: "orphan", + directory: "/", + title: "Orphan", + version: "1.0.0", + time: { created: Date.now(), updated: Date.now() }, + }), + ) + + const stats = await migrateFromJson(sqlite, storageDir) + + expect(stats?.sessions).toBe(0) + }) + + test("creates sqlite-migrated marker file", async () => { + await migrateFromJson(sqlite, storageDir) + + const marker = path.join(storageDir, "sqlite-migrated") + expect(await Bun.file(marker).exists()).toBe(true) + }) + + test("skips if already migrated", async () => { + await Bun.write(path.join(storageDir, "sqlite-migrated"), Date.now().toString()) + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + + const stats = await migrateFromJson(sqlite, storageDir) + + // Should return undefined (skipped) since already migrated + expect(stats).toBeUndefined() + }) + + test("is idempotent (running twice doesn't duplicate)", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + + await migrateFromJson(sqlite, storageDir) + + // Remove marker to run again + await fs.rm(path.join(storageDir, "sqlite-migrated")) + + await migrateFromJson(sqlite, storageDir) + + const db = drizzle(sqlite) + const projects = db.select().from(ProjectTable).all() + expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing + }) +}) From 105688bf90f534a6320822382aacfee013e5204e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 25 Jan 2026 20:16:56 -0500 Subject: [PATCH 02/17] sync --- packages/opencode/package.json | 2 +- packages/opencode/src/cli/cmd/database.ts | 20 ++-- packages/opencode/src/cli/cmd/import.ts | 44 ++++---- packages/opencode/src/cli/cmd/stats.ts | 4 +- packages/opencode/src/permission/next.ts | 7 +- packages/opencode/src/project/project.ts | 107 +++++++++++--------- packages/opencode/src/session/index.ts | 73 +++++++------ packages/opencode/src/session/message-v2.ts | 21 ++-- packages/opencode/src/session/revert.ts | 19 ++-- packages/opencode/src/session/summary.ts | 19 ++-- packages/opencode/src/session/todo.ts | 17 ++-- packages/opencode/src/share/share-next.ts | 21 ++-- packages/opencode/src/storage/db.ts | 103 ++++++++++++++----- packages/opencode/src/worktree/index.ts | 5 +- 14 files changed, 275 insertions(+), 187 deletions(-) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6c3f3a5cca2..a3b00771d31 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -97,8 +97,8 @@ "chokidar": "4.0.3", "clipboardy": "4.0.0", "decimal.js": "10.5.0", - "drizzle-orm": "0.44.2", "diff": "catalog:", + "drizzle-orm": "0.44.2", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts index 5b3c1485f30..949f128ba64 100644 --- a/packages/opencode/src/cli/cmd/database.ts +++ b/packages/opencode/src/cli/cmd/database.ts @@ -2,7 +2,7 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { UI } from "../ui" -import { db } from "../../storage/db" +import { Database } from "../../storage/db" import { ProjectTable } from "../../project/project.sql" import { Project } from "../../project/project" import { @@ -56,7 +56,7 @@ const ExportCommand = cmd({ // Export projects const projectDir = path.join(outDir, "project") await fs.mkdir(projectDir, { recursive: true }) - for (const row of db().select().from(ProjectTable).all()) { + 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++ @@ -64,7 +64,7 @@ const ExportCommand = cmd({ // Export sessions (organized by projectID) const sessionDir = path.join(outDir, "session") - for (const row of db().select().from(SessionTable).all()) { + for (const row of Database.use((db) => db.select().from(SessionTable).all())) { const dir = path.join(sessionDir, row.projectID) await fs.mkdir(dir, { recursive: true }) await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(Session.fromRow(row), null, 2)) @@ -73,7 +73,7 @@ const ExportCommand = cmd({ // Export messages (organized by sessionID) const messageDir = path.join(outDir, "message") - for (const row of db().select().from(MessageTable).all()) { + for (const row of Database.use((db) => db.select().from(MessageTable).all())) { const dir = path.join(messageDir, row.sessionID) await fs.mkdir(dir, { recursive: true }) await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) @@ -82,7 +82,7 @@ const ExportCommand = cmd({ // Export parts (organized by messageID) const partDir = path.join(outDir, "part") - for (const row of db().select().from(PartTable).all()) { + for (const row of Database.use((db) => db.select().from(PartTable).all())) { const dir = path.join(partDir, row.messageID) await fs.mkdir(dir, { recursive: true }) await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) @@ -92,7 +92,7 @@ const ExportCommand = cmd({ // Export session diffs const diffDir = path.join(outDir, "session_diff") await fs.mkdir(diffDir, { recursive: true }) - for (const row of db().select().from(SessionDiffTable).all()) { + for (const row of Database.use((db) => db.select().from(SessionDiffTable).all())) { await Bun.write(path.join(diffDir, `${row.sessionID}.json`), JSON.stringify(row.data, null, 2)) stats.diffs++ } @@ -100,7 +100,7 @@ const ExportCommand = cmd({ // Export todos const todoDir = path.join(outDir, "todo") await fs.mkdir(todoDir, { recursive: true }) - for (const row of db().select().from(TodoTable).all()) { + for (const row of Database.use((db) => db.select().from(TodoTable).all())) { await Bun.write(path.join(todoDir, `${row.sessionID}.json`), JSON.stringify(row.data, null, 2)) stats.todos++ } @@ -108,7 +108,7 @@ const ExportCommand = cmd({ // Export permissions const permDir = path.join(outDir, "permission") await fs.mkdir(permDir, { recursive: true }) - for (const row of db().select().from(PermissionTable).all()) { + for (const row of Database.use((db) => db.select().from(PermissionTable).all())) { await Bun.write(path.join(permDir, `${row.projectID}.json`), JSON.stringify(row.data, null, 2)) stats.permissions++ } @@ -116,7 +116,7 @@ const ExportCommand = cmd({ // Export session shares const sessionShareDir = path.join(outDir, "session_share") await fs.mkdir(sessionShareDir, { recursive: true }) - for (const row of db().select().from(SessionShareTable).all()) { + for (const row of Database.use((db) => db.select().from(SessionShareTable).all())) { await Bun.write(path.join(sessionShareDir, `${row.sessionID}.json`), JSON.stringify(row.data, null, 2)) stats.sessionShares++ } @@ -124,7 +124,7 @@ const ExportCommand = cmd({ // Export shares const shareDir = path.join(outDir, "share") await fs.mkdir(shareDir, { recursive: true }) - for (const row of db().select().from(ShareTable).all()) { + for (const row of Database.use((db) => db.select().from(ShareTable).all())) { await Bun.write(path.join(shareDir, `${row.sessionID}.json`), JSON.stringify(row.data, null, 2)) stats.shares++ } diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index c78776b9d76..7f97b70c317 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -2,7 +2,7 @@ import type { Argv } from "yargs" import { Session } from "../../session" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" -import { db } from "../../storage/db" +import { Database } from "../../storage/db" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { Instance } from "../../project/instance" import { EOL } from "os" @@ -82,31 +82,35 @@ export const ImportCommand = cmd({ return } - db().insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run() + Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run()) for (const msg of exportData.messages) { - db() - .insert(MessageTable) - .values({ - id: msg.info.id, - sessionID: exportData.info.id, - createdAt: msg.info.time?.created ?? Date.now(), - data: msg.info, - }) - .onConflictDoNothing() - .run() - - for (const part of msg.parts) { - db() - .insert(PartTable) + Database.use((db) => + db + .insert(MessageTable) .values({ - id: part.id, - messageID: msg.info.id, + id: msg.info.id, sessionID: exportData.info.id, - data: part, + createdAt: msg.info.time?.created ?? Date.now(), + data: msg.info, }) .onConflictDoNothing() - .run() + .run(), + ) + + for (const part of msg.parts) { + Database.use((db) => + db + .insert(PartTable) + .values({ + id: part.id, + messageID: msg.info.id, + sessionID: exportData.info.id, + data: part, + }) + .onConflictDoNothing() + .run(), + ) } } diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 21ee97fa826..1f7263b3252 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -2,7 +2,7 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { Session } from "../../session" import { bootstrap } from "../bootstrap" -import { db } from "../../storage/db" +import { Database } from "../../storage/db" import { SessionTable } from "../../session/session.sql" import { Project } from "../../project/project" import { Instance } from "../../project/instance" @@ -84,7 +84,7 @@ async function getCurrentProject(): Promise { } async function getAllSessions(): Promise { - const rows = db().select().from(SessionTable).all() + const rows = Database.use((db) => db.select().from(SessionTable).all()) return rows.map((row) => Session.fromRow(row)) } diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index b625bb57fc5..98840867b2f 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -3,9 +3,8 @@ import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" import { Identifier } from "@/id/id" import { Instance } from "@/project/instance" -import { db } from "@/storage/db" +import { Database, eq } from "@/storage/db" import { PermissionTable } from "@/session/session.sql" -import { eq } from "drizzle-orm" import { fn } from "@/util/fn" import { Log } from "@/util/log" import { Wildcard } from "@/util/wildcard" @@ -109,7 +108,9 @@ export namespace PermissionNext { const state = Instance.state(() => { const projectID = Instance.project.id - const row = db().select().from(PermissionTable).where(eq(PermissionTable.projectID, projectID)).get() + const row = Database.use((db) => + db.select().from(PermissionTable).where(eq(PermissionTable.projectID, projectID)).get(), + ) const stored = row?.data ?? ([] as Ruleset) const pending: Record< diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 71c6a9bc7b0..fc940b95884 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -3,10 +3,9 @@ import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import path from "path" import { $ } from "bun" -import { db } from "../storage/db" +import { Database, eq } from "../storage/db" import { ProjectTable } from "./project.sql" import { SessionTable } from "../session/session.sql" -import { eq } from "drizzle-orm" import { Log } from "../util/log" import { Flag } from "@/flag/flag" import { work } from "../util/queue" @@ -199,7 +198,7 @@ export namespace Project { } }) - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, id)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const existing = await iife(async () => { if (row) return fromRow(row) const fresh: Info = { @@ -253,7 +252,9 @@ export namespace Project { time_initialized: result.time.initialized, sandboxes: result.sandboxes, } - db().insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run() + Database.use((db) => + db.insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run(), + ) GlobalBus.emit("event", { payload: { type: Event.Updated.type, @@ -294,10 +295,12 @@ export namespace Project { } async function migrateFromGlobal(newProjectID: string, worktree: string) { - const globalRow = db().select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get() + const globalRow = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get()) if (!globalRow) return - const globalSessions = db().select().from(SessionTable).where(eq(SessionTable.projectID, "global")).all() + const globalSessions = Database.use((db) => + db.select().from(SessionTable).where(eq(SessionTable.projectID, "global")).all(), + ) if (globalSessions.length === 0) return log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length }) @@ -307,32 +310,38 @@ export namespace Project { if (row.directory && row.directory !== worktree) return log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID }) - db().update(SessionTable).set({ projectID: newProjectID }).where(eq(SessionTable.id, row.id)).run() + Database.use((db) => + db.update(SessionTable).set({ projectID: newProjectID }).where(eq(SessionTable.id, row.id)).run(), + ) }).catch((error) => { log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID }) }) } export function setInitialized(projectID: string) { - db() - .update(ProjectTable) - .set({ - time_initialized: Date.now(), - }) - .where(eq(ProjectTable.id, projectID)) - .run() + Database.use((db) => + db + .update(ProjectTable) + .set({ + time_initialized: Date.now(), + }) + .where(eq(ProjectTable.id, projectID)) + .run(), + ) } export function list() { - return db() - .select() - .from(ProjectTable) - .all() - .map((row) => fromRow(row)) + return Database.use((db) => + db + .select() + .from(ProjectTable) + .all() + .map((row) => fromRow(row)), + ) } export function get(projectID: string): Info | undefined { - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) if (!row) return undefined return fromRow(row) } @@ -345,17 +354,19 @@ export namespace Project { commands: Info.shape.commands.optional(), }), async (input) => { - const result = db() - .update(ProjectTable) - .set({ - name: input.name, - icon_url: input.icon?.url, - icon_color: input.icon?.color, - time_updated: Date.now(), - }) - .where(eq(ProjectTable.id, input.projectID)) - .returning() - .get() + const result = Database.use((db) => + db + .update(ProjectTable) + .set({ + name: input.name, + icon_url: input.icon?.url, + icon_color: input.icon?.color, + time_updated: Date.now(), + }) + .where(eq(ProjectTable.id, input.projectID)) + .returning() + .get(), + ) if (!result) throw new Error(`Project not found: ${input.projectID}`) const data = fromRow(result) GlobalBus.emit("event", { @@ -369,7 +380,7 @@ export namespace Project { ) export async function sandboxes(projectID: string) { - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) if (!row) return [] const data = fromRow(row) const valid: string[] = [] @@ -381,16 +392,18 @@ export namespace Project { } export async function addSandbox(projectID: string, directory: string) { - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) if (!row) throw new Error(`Project not found: ${projectID}`) const sandboxes = [...row.sandboxes] if (!sandboxes.includes(directory)) sandboxes.push(directory) - const result = db() - .update(ProjectTable) - .set({ sandboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, projectID)) - .returning() - .get() + const result = Database.use((db) => + db + .update(ProjectTable) + .set({ sandboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, projectID)) + .returning() + .get(), + ) if (!result) throw new Error(`Project not found: ${projectID}`) const data = fromRow(result) GlobalBus.emit("event", { @@ -403,15 +416,17 @@ export namespace Project { } export async function removeSandbox(projectID: string, directory: string) { - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) if (!row) throw new Error(`Project not found: ${projectID}`) const sandboxes = row.sandboxes.filter((s: string) => s !== directory) - const result = db() - .update(ProjectTable) - .set({ sandboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, projectID)) - .returning() - .get() + const result = Database.use((db) => + db + .update(ProjectTable) + .set({ sandboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, projectID)) + .returning() + .get(), + ) if (!result) throw new Error(`Project not found: ${projectID}`) const data = fromRow(result) GlobalBus.emit("event", { diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 84b567b9425..db06483eea2 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,10 +10,9 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { db, NotFoundError } from "../storage/db" +import { Database, NotFoundError, eq } from "../storage/db" import { SessionTable, MessageTable, PartTable, SessionDiffTable } from "./session.sql" import { ShareTable } from "../share/share.sql" -import { eq } from "drizzle-orm" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" @@ -283,7 +282,7 @@ export namespace Session { }, } log.info("created", result) - db().insert(SessionTable).values(toRow(result)).run() + Database.use((db) => db.insert(SessionTable).values(toRow(result)).run()) Bus.publish(Event.Created, { info: result, }) @@ -312,13 +311,13 @@ export namespace Session { } export const get = fn(Identifier.schema("session"), async (id) => { - const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get() + const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) return fromRow(row) }) export const getShare = fn(Identifier.schema("session"), async (id) => { - const row = db().select().from(ShareTable).where(eq(ShareTable.sessionID, id)).get() + const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.sessionID, id)).get()) return row?.data }) @@ -355,14 +354,14 @@ export namespace Session { }) export function update(id: string, editor: (session: Info) => void, options?: { touch?: boolean }) { - const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get() + const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) if (!row) throw new Error(`Session not found: ${id}`) const data = fromRow(row) editor(data) if (options?.touch !== false) { data.time.updated = Date.now() } - db().update(SessionTable).set(toRow(data)).where(eq(SessionTable.id, id)).run() + Database.use((db) => db.update(SessionTable).set(toRow(data)).where(eq(SessionTable.id, id)).run()) Bus.publish(Event.Updated, { info: data, }) @@ -370,7 +369,9 @@ export namespace Session { } export const diff = fn(Identifier.schema("session"), async (sessionID) => { - const row = db().select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, sessionID)).get() + const row = Database.use((db) => + db.select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, sessionID)).get(), + ) return row?.data ?? [] }) @@ -392,14 +393,16 @@ export namespace Session { export function* list() { const project = Instance.project - const rows = db().select().from(SessionTable).where(eq(SessionTable.projectID, project.id)).all() + const rows = Database.use((db) => + db.select().from(SessionTable).where(eq(SessionTable.projectID, project.id)).all(), + ) for (const row of rows) { yield fromRow(row) } } export const children = fn(Identifier.schema("session"), async (parentID) => { - const rows = db().select().from(SessionTable).where(eq(SessionTable.parentID, parentID)).all() + const rows = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.parentID, parentID)).all()) return rows.map((row) => fromRow(row)) }) @@ -412,7 +415,7 @@ export namespace Session { } await unshare(sessionID).catch(() => {}) // CASCADE delete handles messages and parts automatically - db().delete(SessionTable).where(eq(SessionTable.id, sessionID)).run() + Database.use((db) => db.delete(SessionTable).where(eq(SessionTable.id, sessionID)).run()) Bus.publish(Event.Deleted, { info: session, }) @@ -423,16 +426,18 @@ export namespace Session { export const updateMessage = fn(MessageV2.Info, async (msg) => { const createdAt = msg.role === "user" ? msg.time.created : msg.time.created - db() - .insert(MessageTable) - .values({ - id: msg.id, - sessionID: msg.sessionID, - createdAt, - data: msg, - }) - .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) - .run() + Database.use((db) => + db + .insert(MessageTable) + .values({ + id: msg.id, + sessionID: msg.sessionID, + createdAt, + data: msg, + }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) + .run(), + ) Bus.publish(MessageV2.Event.Updated, { info: msg, }) @@ -446,7 +451,7 @@ export namespace Session { }), async (input) => { // CASCADE delete handles parts automatically - db().delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run() + Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run()) Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: input.messageID, @@ -462,7 +467,7 @@ export namespace Session { partID: Identifier.schema("part"), }), async (input) => { - db().delete(PartTable).where(eq(PartTable.id, input.partID)).run() + Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, input.partID)).run()) Bus.publish(MessageV2.Event.PartRemoved, { sessionID: input.sessionID, messageID: input.messageID, @@ -487,16 +492,18 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (input) => { const part = "delta" in input ? input.part : input const delta = "delta" in input ? input.delta : undefined - db() - .insert(PartTable) - .values({ - id: part.id, - messageID: part.messageID, - sessionID: part.sessionID, - data: part, - }) - .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) - .run() + Database.use((db) => + db + .insert(PartTable) + .values({ + id: part.id, + messageID: part.messageID, + sessionID: part.sessionID, + data: part, + }) + .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) + .run(), + ) Bus.publish(MessageV2.Event.PartUpdated, { part, delta, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2dab09918ae..e92252400c1 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -6,9 +6,8 @@ import { Identifier } from "../id/id" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" import { fn } from "@/util/fn" -import { db } from "@/storage/db" +import { Database, eq, desc } from "@/storage/db" import { MessageTable, PartTable } from "./session.sql" -import { eq, desc } from "drizzle-orm" import { ProviderTransform } from "@/provider/transform" import { STATUS_CODES } from "http" import { iife } from "@/util/iife" @@ -609,12 +608,14 @@ export namespace MessageV2 { } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { - const rows = db() - .select() - .from(MessageTable) - .where(eq(MessageTable.sessionID, sessionID)) - .orderBy(desc(MessageTable.createdAt)) - .all() + const rows = Database.use((db) => + db + .select() + .from(MessageTable) + .where(eq(MessageTable.sessionID, sessionID)) + .orderBy(desc(MessageTable.createdAt)) + .all(), + ) for (const row of rows) { yield { info: row.data, @@ -624,7 +625,7 @@ export namespace MessageV2 { }) export const parts = fn(Identifier.schema("message"), async (messageID) => { - const rows = db().select().from(PartTable).where(eq(PartTable.messageID, messageID)).all() + const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.messageID, messageID)).all()) const result = rows.map((row) => row.data) result.sort((a, b) => (a.id > b.id ? 1 : -1)) return result @@ -636,7 +637,7 @@ export namespace MessageV2 { messageID: Identifier.schema("message"), }), async (input) => { - const row = db().select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get() + const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get()) if (!row) throw new Error(`Message not found: ${input.messageID}`) return { info: row.data, diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index fb6e0a5ec39..6a27fc7b9a4 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -5,9 +5,8 @@ import { MessageV2 } from "./message-v2" import { Session } from "." import { Log } from "../util/log" import { splitWhen } from "remeda" -import { db } from "../storage/db" +import { Database, eq } from "../storage/db" import { SessionDiffTable, MessageTable, PartTable } from "./session.sql" -import { eq } from "drizzle-orm" import { Bus } from "../bus" import { SessionPrompt } from "./prompt" import { SessionSummary } from "./summary" @@ -62,11 +61,13 @@ 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 }) - db() - .insert(SessionDiffTable) - .values({ sessionID: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) - .run() + Database.use((db) => + db + .insert(SessionDiffTable) + .values({ sessionID: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .run(), + ) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -103,7 +104,7 @@ export namespace SessionRevert { const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID) msgs = preserve for (const msg of remove) { - db().delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run() + Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run()) await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id }) } const last = preserve.at(-1) @@ -112,7 +113,7 @@ export namespace SessionRevert { const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID) last.parts = preserveParts for (const part of removeParts) { - db().delete(PartTable).where(eq(PartTable.id, part.id)).run() + Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, part.id)).run()) await Bus.publish(MessageV2.Event.PartRemoved, { sessionID: sessionID, messageID: last.info.id, diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index a79850046d9..2472eee7a39 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -11,9 +11,8 @@ import { Snapshot } from "@/snapshot" import { Log } from "@/util/log" import path from "path" import { Instance } from "@/project/instance" -import { db } from "@/storage/db" +import { Database, eq } from "@/storage/db" import { SessionDiffTable } from "./session.sql" -import { eq } from "drizzle-orm" import { Bus } from "@/bus" import { LLM } from "./llm" @@ -56,11 +55,13 @@ export namespace SessionSummary { files: diffs.length, } }) - db() - .insert(SessionDiffTable) - .values({ sessionID: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) - .run() + Database.use((db) => + db + .insert(SessionDiffTable) + .values({ sessionID: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .run(), + ) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -122,7 +123,9 @@ export namespace SessionSummary { messageID: Identifier.schema("message").optional(), }), async (input) => { - const row = db().select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, input.sessionID)).get() + const row = Database.use((db) => + db.select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, input.sessionID)).get(), + ) return row?.data ?? [] }, ) diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 3280744662d..8ba5a0281cc 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,9 +1,8 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" -import { db } from "../storage/db" +import { Database, eq } from "../storage/db" import { TodoTable } from "./session.sql" -import { eq } from "drizzle-orm" export namespace Todo { export const Info = z @@ -27,16 +26,18 @@ export namespace Todo { } export function update(input: { sessionID: string; todos: Info[] }) { - db() - .insert(TodoTable) - .values({ sessionID: input.sessionID, data: input.todos }) - .onConflictDoUpdate({ target: TodoTable.sessionID, set: { data: input.todos } }) - .run() + Database.use((db) => + db + .insert(TodoTable) + .values({ sessionID: input.sessionID, data: input.todos }) + .onConflictDoUpdate({ target: TodoTable.sessionID, set: { data: input.todos } }) + .run(), + ) Bus.publish(Event.Updated, input) } export function get(sessionID: string) { - const row = db().select().from(TodoTable).where(eq(TodoTable.sessionID, sessionID)).get() + const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.sessionID, sessionID)).get()) return row?.data ?? [] } } diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 2d168204592..0f18cb974db 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -4,9 +4,8 @@ import { ulid } from "ulid" import { Provider } from "@/provider/provider" import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" -import { db } from "@/storage/db" +import { Database, eq } from "@/storage/db" import { SessionShareTable } from "./share.sql" -import { eq } from "drizzle-orm" import { Log } from "@/util/log" import type * as SDK from "@opencode-ai/sdk/v2" @@ -79,17 +78,21 @@ export namespace ShareNext { }) .then((x) => x.json()) .then((x) => x as { id: string; url: string; secret: string }) - db() - .insert(SessionShareTable) - .values({ sessionID, data: result }) - .onConflictDoUpdate({ target: SessionShareTable.sessionID, set: { data: result } }) - .run() + Database.use((db) => + db + .insert(SessionShareTable) + .values({ sessionID, data: result }) + .onConflictDoUpdate({ target: SessionShareTable.sessionID, set: { data: result } }) + .run(), + ) fullSync(sessionID) return result } function get(sessionID: string) { - const row = db().select().from(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).get() + const row = Database.use((db) => + db.select().from(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).get(), + ) return row?.data } @@ -166,7 +169,7 @@ export namespace ShareNext { secret: share.secret, }), }) - db().delete(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).run() + Database.use((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).run()) } async function fullSync(sessionID: string) { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 3c1a159305c..475da9475b6 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,5 +1,9 @@ -import { Database } from "bun:sqlite" -import { drizzle } from "drizzle-orm/bun-sqlite" +import { Database as BunDatabase } from "bun:sqlite" +import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite" +import type { SQLiteTransaction } from "drizzle-orm/sqlite-core" +import type { ExtractTablesWithRelations } from "drizzle-orm" +export * from "drizzle-orm" +import { Context } from "../util/context" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util/log" @@ -18,29 +22,85 @@ export const NotFoundError = NamedError.create( const log = Log.create({ service: "db" }) -export type DB = ReturnType +export namespace Database { + export type Transaction = SQLiteTransaction< + "sync", + void, + Record, + ExtractTablesWithRelations> + > -const connection = lazy(() => { - const dbPath = path.join(Global.Path.data, "opencode.db") - log.info("opening database", { path: dbPath }) + type Client = BunSQLiteDatabase> - const sqlite = new Database(dbPath, { create: true }) + const client = lazy(() => { + const dbPath = path.join(Global.Path.data, "opencode.db") + log.info("opening database", { path: dbPath }) - sqlite.run("PRAGMA journal_mode = WAL") - sqlite.run("PRAGMA synchronous = NORMAL") - sqlite.run("PRAGMA busy_timeout = 5000") - sqlite.run("PRAGMA cache_size = -64000") - sqlite.run("PRAGMA foreign_keys = ON") + const sqlite = new BunDatabase(dbPath, { create: true }) - migrate(sqlite) + sqlite.run("PRAGMA journal_mode = WAL") + sqlite.run("PRAGMA synchronous = NORMAL") + sqlite.run("PRAGMA busy_timeout = 5000") + sqlite.run("PRAGMA cache_size = -64000") + sqlite.run("PRAGMA foreign_keys = ON") - // Run JSON migration asynchronously after schema is ready - migrateFromJson(sqlite).catch((e) => log.error("json migration failed", { error: e })) + migrate(sqlite) - return drizzle(sqlite) -}) + migrateFromJson(sqlite).catch((e) => log.error("json migration failed", { error: e })) -function migrate(sqlite: Database) { + return drizzle(sqlite) + }) + + export type TxOrDb = Transaction | Client + + const TransactionContext = Context.create<{ + tx: TxOrDb + effects: (() => void | Promise)[] + }>("database") + + export function use(callback: (trx: TxOrDb) => T): T { + try { + const { tx } = TransactionContext.use() + return callback(tx) + } catch (err) { + if (err instanceof Context.NotFound) { + const effects: (() => void | Promise)[] = [] + const result = TransactionContext.provide({ effects, tx: client() }, () => callback(client())) + for (const effect of effects) effect() + return result + } + throw err + } + } + + export function effect(effect: () => void | Promise) { + try { + const { effects } = TransactionContext.use() + effects.push(effect) + } catch { + effect() + } + } + + export function transaction(callback: (tx: TxOrDb) => T): T { + try { + const { tx } = TransactionContext.use() + return callback(tx) + } catch (err) { + if (err instanceof Context.NotFound) { + const effects: (() => void | Promise)[] = [] + const result = client().transaction((tx) => { + return TransactionContext.provide({ tx, effects }, () => callback(tx)) + }) + for (const effect of effects) effect() + return result + } + throw err + } + } +} + +function migrate(sqlite: BunDatabase) { sqlite.exec(` CREATE TABLE IF NOT EXISTS _migrations ( name TEXT PRIMARY KEY, @@ -59,8 +119,6 @@ function migrate(sqlite: Database) { if (applied.has(migration.name)) continue log.info("applying migration", { name: migration.name }) - // Split by statement breakpoint and execute each statement - // Use IF NOT EXISTS variants to handle partial migrations const statements = migration.sql.split("--> statement-breakpoint") for (const stmt of statements) { const trimmed = stmt.trim() @@ -69,7 +127,6 @@ function migrate(sqlite: Database) { try { sqlite.exec(trimmed) } catch (e: any) { - // Ignore "already exists" errors for idempotency if (e?.message?.includes("already exists")) { log.info("skipping existing object", { statement: trimmed.slice(0, 50) }) continue @@ -81,7 +138,3 @@ function migrate(sqlite: Database) { sqlite.run("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)", [migration.name, Date.now()]) } } - -export function db() { - return connection() -} diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 30443d36b1b..0afffda999f 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -7,9 +7,8 @@ import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project/project" -import { db } from "../storage/db" +import { Database, eq } from "../storage/db" import { ProjectTable } from "../project/project.sql" -import { eq } from "drizzle-orm" import { fn } from "../util/fn" import { Log } from "../util/log" import { BusEvent } from "@/bus/bus-event" @@ -320,7 +319,7 @@ export namespace Worktree { }, }) - const row = db().select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) const project = row ? Project.fromRow(row) : undefined const startup = project?.commands?.start?.trim() ?? "" From b9f5a34247137a057be1e0e25441d48887c8c984 Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 26 Jan 2026 02:32:52 +0000 Subject: [PATCH 03/17] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index d5559fdfb10..af22d7711ab 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=", - "aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=", - "aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=", - "x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ=" + "x86_64-linux": "sha256-EAvkIGLyoGd5XJAvrp5Zox5q5hzeIYqnLqqFiXZXCh4=", + "aarch64-linux": "sha256-LCCWwwJPgm0PPkqujjWZDf3L4pMo2epvmT2HUjwp2RA=", + "aarch64-darwin": "sha256-K3tUTy7hR2mYyq+mWQPcnnAVQgZF32FgrbHoqX58h8o=", + "x86_64-darwin": "sha256-F/C2dezX+sKH0ZcmUGSxEXkEXWI6YGhOeVdF3wxF94M=" } } From a614b78c6deae955f531eef8e684e95851052ae4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 25 Jan 2026 22:26:52 -0500 Subject: [PATCH 04/17] tui: upgrade database migration system to drizzle migrator Replaces custom migration system with drizzle-orm's built-in migrator, bundling migrations at build-time instead of runtime generation. This reduces bundle complexity and provides better integration with drizzle's migration tracking. --- AGENTS.md | 101 ++++++++------- bun.lock | 120 +++++++++++++++++- packages/opencode/package.json | 7 +- packages/opencode/script/build.ts | 13 ++ .../opencode/script/generate-migrations.ts | 49 ------- packages/opencode/src/cli/cmd/database.ts | 3 - packages/opencode/src/storage/db.ts | 107 ++++++++-------- .../opencode/src/storage/json-migration.ts | 17 +-- .../src/storage/migrations.generated.ts | 6 - .../test/storage/json-migration.test.ts | 27 ++-- 10 files changed, 260 insertions(+), 190 deletions(-) delete mode 100644 packages/opencode/script/generate-migrations.ts delete mode 100644 packages/opencode/src/storage/migrations.generated.ts diff --git a/AGENTS.md b/AGENTS.md index c157fa32d3b..6119e653e1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,80 +1,93 @@ -- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. -- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. -- The default branch in this repo is `dev`. +# Agent Instructions + +## Quick Reference + +- Default branch: `dev` +- Regenerate JS SDK: `./packages/sdk/js/script/build.ts` +- Always use parallel tools when applicable ## Style Guide +### General Principles + - Keep things in one function unless composable or reusable -- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context - Avoid `try`/`catch` where possible - Avoid using the `any` type -- Prefer single word variable names where possible -- Use Bun APIs when possible, like `Bun.file()` +- Use Bun APIs when possible (e.g., `Bun.file()` instead of `fs.existsSync()`) +- For sync file reads, use `readFileSync` from `fs` (Bun.file is async-only) +- Avoid generated file artifacts - prefer build-time `define` globals for bundled data -### Avoid let statements +### Naming -We don't like `let` statements, especially combined with if/else statements. -Prefer `const`. +Prefer single word variable names. Only use multiple words if necessary. + +```ts +// Good +const foo = 1 +const bar = 2 + +// Bad +const fooBar = 1 +const barBaz = 2 +``` -Good: +Reduce total variable count by inlining when a value is only used once. ```ts -const foo = condition ? 1 : 2 +// Good +const journal = await Bun.file(path.join(dir, "journal.json")).json() + +// Bad +const journalPath = path.join(dir, "journal.json") +const journal = await Bun.file(journalPath).json() ``` -Bad: +### Destructuring + +Avoid unnecessary destructuring. Use dot notation to preserve context. ```ts -let foo +// Good +obj.a +obj.b + +// Bad +const { a, b } = obj +``` + +### Variables + +Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. +```ts +// Good +const foo = condition ? 1 : 2 + +// Bad +let foo if (condition) foo = 1 else foo = 2 ``` -### Avoid else statements - -Prefer early returns or using an `iife` to avoid else statements. +### Control Flow -Good: +Avoid `else` statements. Prefer early returns. ```ts +// Good function foo() { if (condition) return 1 return 2 } -``` - -Bad: -```ts +// Bad function foo() { if (condition) return 1 else return 2 } ``` -### Prefer single word naming - -Try your best to find a single word name for your variables, functions, etc. -Only use multiple words if you cannot. - -Good: - -```ts -const foo = 1 -const bar = 2 -const baz = 3 -``` - -Bad: - -```ts -const fooBar = 1 -const barBaz = 2 -const bazFoo = 3 -``` - ## Testing -You MUST avoid using `mocks` as much as possible. -Tests MUST test actual implementation, do not duplicate logic into a test. +- Avoid mocks as much as possible +- Test actual implementation, do not duplicate logic into tests diff --git a/bun.lock b/bun.lock index f65985e9b52..d015c9aea98 100644 --- a/bun.lock +++ b/bun.lock @@ -311,7 +311,7 @@ "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", - "drizzle-orm": "0.44.2", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -353,7 +353,7 @@ "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", - "drizzle-kit": "0.31.0", + "drizzle-kit": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -707,6 +707,8 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], @@ -729,8 +731,20 @@ "@azure/core-xml": ["@azure/core-xml@1.5.0", "", { "dependencies": { "fast-xml-parser": "^5.0.7", "tslib": "^2.8.1" } }, "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw=="], + "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + + "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], + + "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + "@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="], + + "@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="], + + "@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="], + "@azure/storage-blob": ["@azure/storage-blob@12.29.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.1.1", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-7ktyY0rfTM0vo7HvtK6E3UvYnI9qfd6Oz6z/+92VhGRveWng3kJwMKeUpqmW/NmwcDNbxHpSlldG+vsUnRFnBg=="], "@azure/storage-common": ["@azure/storage-common@12.1.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-eIOH1pqFwI6UmVNnDQvmFeSg0XppuzDLFeUNO/Xht7ODAzRLgGDh7h550pSxoA+lPDxBl1+D2m/KG3jWzCUjTg=="], @@ -1069,6 +1083,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], @@ -1769,6 +1787,8 @@ "@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="], + "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1837,6 +1857,8 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], @@ -1853,6 +1875,8 @@ "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], + "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], @@ -2053,6 +2077,8 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], @@ -2833,10 +2859,14 @@ "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -3113,6 +3143,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -3127,6 +3159,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -3483,6 +3517,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], @@ -3597,7 +3633,7 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], @@ -3691,6 +3727,10 @@ "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], + + "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="], "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], @@ -3751,6 +3791,8 @@ "tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], @@ -4069,6 +4111,8 @@ "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -4317,6 +4361,8 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -4407,6 +4453,8 @@ "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], @@ -4421,9 +4469,9 @@ "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@0.31.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg=="], + "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@0.44.2", "", { "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.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@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", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], + "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=="], @@ -4503,12 +4551,16 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], @@ -4957,6 +5009,8 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -5031,6 +5085,8 @@ "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=="], @@ -5059,6 +5115,58 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -5203,6 +5311,8 @@ "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "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=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3fc8b3c4426..c227eac1916 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -15,7 +15,8 @@ "lint": "echo 'Running lint checks...' && bun test --coverage", "format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts", "docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;", - "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'" + "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'", + "db": "bun drizzle-kit" }, "bin": { "opencode": "./bin/opencode" @@ -25,7 +26,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "drizzle-kit": "0.31.0", + "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", @@ -98,7 +99,7 @@ "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", - "drizzle-orm": "0.44.2", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 12902b1cfc8..06ed45e4d2c 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -25,6 +25,18 @@ await Bun.write( ) console.log("Generated models-snapshot.ts") +// Load migrations from journal +const journal = (await Bun.file(path.join(dir, "migration/meta/_journal.json")).json()) as { + entries: { tag: string; when: number }[] +} +const migrations = await Promise.all( + journal.entries.map(async (entry) => { + const sql = await Bun.file(path.join(dir, `migration/${entry.tag}.sql`)).text() + return { sql, timestamp: entry.when } + }), +) +console.log(`Loaded ${migrations.length} migrations`) + const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") @@ -156,6 +168,7 @@ for (const item of targets) { entrypoints: ["./src/index.ts", parserWorker, workerPath], define: { OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_MIGRATIONS: JSON.stringify(migrations), OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, OPENCODE_WORKER_PATH: workerPath, OPENCODE_CHANNEL: `'${Script.channel}'`, diff --git a/packages/opencode/script/generate-migrations.ts b/packages/opencode/script/generate-migrations.ts deleted file mode 100644 index 47c2e0c5e11..00000000000 --- a/packages/opencode/script/generate-migrations.ts +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bun - -import { Glob } from "bun" -import path from "path" -import fs from "fs" - -const migrationsDir = "./migration" -const outFile = "./src/storage/migrations.generated.ts" - -if (!fs.existsSync(migrationsDir)) { - console.log("No migrations directory found, creating empty migrations file") - await Bun.write( - outFile, - `// Auto-generated - do not edit -export const migrations: { name: string; sql: string }[] = [] -`, - ) - process.exit(0) -} - -const files = Array.from(new Glob("*.sql").scanSync({ cwd: migrationsDir })).sort() - -if (files.length === 0) { - console.log("No migrations found, creating empty migrations file") - await Bun.write( - outFile, - `// Auto-generated - do not edit -export const migrations: { name: string; sql: string }[] = [] -`, - ) - process.exit(0) -} - -const imports = files.map((f, i) => `import m${i} from "../../migration/${f}" with { type: "text" }`).join("\n") - -const entries = files.map((f, i) => ` { name: "${path.basename(f, ".sql")}", sql: m${i} },`).join("\n") - -await Bun.write( - outFile, - `// Auto-generated - do not edit -${imports} - -export const migrations = [ -${entries} -] -`, -) - -console.log(`Generated migrations file with ${files.length} migrations`) diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts index 949f128ba64..1fda74cf6b4 100644 --- a/packages/opencode/src/cli/cmd/database.ts +++ b/packages/opencode/src/cli/cmd/database.ts @@ -129,9 +129,6 @@ const ExportCommand = cmd({ stats.shares++ } - // Create migration marker so this can be imported back - await Bun.write(path.join(outDir, "migration"), Date.now().toString()) - UI.println(`Exported to ${outDir}:`) UI.println(` ${stats.projects} projects`) UI.println(` ${stats.sessions} sessions`) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 475da9475b6..824bc856584 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,17 +1,19 @@ import { Database as BunDatabase } from "bun:sqlite" -import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite" +import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" +import { migrate as drizzleMigrate } from "drizzle-orm/bun-sqlite/migrator" import type { SQLiteTransaction } from "drizzle-orm/sqlite-core" -import type { ExtractTablesWithRelations } from "drizzle-orm" export * from "drizzle-orm" import { Context } from "../util/context" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util/log" -import { migrations } from "./migrations.generated" import { migrateFromJson } from "./json-migration" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" +import { readFileSync } from "fs" + +declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined export const NotFoundError = NamedError.create( "NotFoundError", @@ -23,20 +25,14 @@ export const NotFoundError = NamedError.create( const log = Log.create({ service: "db" }) export namespace Database { - export type Transaction = SQLiteTransaction< - "sync", - void, - Record, - ExtractTablesWithRelations> - > + export type Transaction = SQLiteTransaction<"sync", void, Record, Record> - type Client = BunSQLiteDatabase> + type Client = SQLiteBunDatabase const client = lazy(() => { - const dbPath = path.join(Global.Path.data, "opencode.db") - log.info("opening database", { path: dbPath }) + log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) - const sqlite = new BunDatabase(dbPath, { create: true }) + const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true }) sqlite.run("PRAGMA journal_mode = WAL") sqlite.run("PRAGMA synchronous = NORMAL") @@ -44,11 +40,24 @@ export namespace Database { sqlite.run("PRAGMA cache_size = -64000") sqlite.run("PRAGMA foreign_keys = ON") - migrate(sqlite) - - migrateFromJson(sqlite).catch((e) => log.error("json migration failed", { error: e })) + const db = drizzle({ client: sqlite }) + migrate(db) + + // Run json migration if not already done + const marker = sqlite.prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = 'json-migration'").get() + if (!marker) { + 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 drizzle(sqlite) + return db }) export type TxOrDb = Transaction | Client @@ -100,41 +109,35 @@ export namespace Database { } } -function migrate(sqlite: BunDatabase) { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS _migrations ( - name TEXT PRIMARY KEY, - applied_at INTEGER NOT NULL - ) - `) - - const applied = new Set( - sqlite - .query<{ name: string }, []>("SELECT name FROM _migrations") - .all() - .map((r) => r.name), - ) - - for (const migration of migrations) { - if (applied.has(migration.name)) continue - log.info("applying migration", { name: migration.name }) - - const statements = migration.sql.split("--> statement-breakpoint") - for (const stmt of statements) { - const trimmed = stmt.trim() - if (!trimmed) continue - - try { - sqlite.exec(trimmed) - } catch (e: any) { - if (e?.message?.includes("already exists")) { - log.info("skipping existing object", { statement: trimmed.slice(0, 50) }) - continue - } - throw e - } - } +type MigrationsJournal = { sql: string; timestamp: number }[] + +function prepareJournal(dir: string): MigrationsJournal { + const file = path.join(dir, "meta/_journal.json") + if (!Bun.file(file).size) return [] - sqlite.run("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)", [migration.name, Date.now()]) + const journal = JSON.parse(readFileSync(file, "utf-8")) as { + entries: { tag: string; when: number }[] } + + return journal.entries.map((entry) => ({ + sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), + timestamp: entry.when, + })) +} + +function migrate(db: SQLiteBunDatabase) { + const journal = + typeof OPENCODE_MIGRATIONS !== "undefined" + ? OPENCODE_MIGRATIONS + : prepareJournal(path.join(import.meta.dirname, "../../migration")) + + if (journal.length === 0) { + log.info("no migrations found") + return + } + log.info("applying migrations", { + count: journal.length, + mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", + }) + drizzleMigrate(db, journal) } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 96f3714c2ac..df15ff6df04 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -19,22 +19,10 @@ const log = Log.create({ service: "json-migration" }) export async function migrateFromJson(sqlite: Database, customStorageDir?: string) { const storageDir = customStorageDir ?? path.join(Global.Path.data, "storage") - const migrationMarker = path.join(storageDir, "sqlite-migrated") - - if (await Bun.file(migrationMarker).exists()) { - log.info("json migration already completed") - return - } - - if (!(await Bun.file(path.join(storageDir, "migration")).exists())) { - log.info("no json storage found, skipping migration") - await Bun.write(migrationMarker, Date.now().toString()) - return - } log.info("starting json to sqlite migration", { storageDir }) - const db = drizzle(sqlite) + const db = drizzle({ client: sqlite }) const stats = { projects: 0, sessions: 0, @@ -277,9 +265,6 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin } } - // Mark migration complete - await Bun.write(migrationMarker, Date.now().toString()) - log.info("json migration complete", { projects: stats.projects, sessions: stats.sessions, diff --git a/packages/opencode/src/storage/migrations.generated.ts b/packages/opencode/src/storage/migrations.generated.ts deleted file mode 100644 index a048c61efb2..00000000000 --- a/packages/opencode/src/storage/migrations.generated.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Auto-generated - do not edit -import m0 from "../../migration/0000_magical_strong_guy.sql" with { type: "text" } - -export const migrations = [ - { name: "0000_magical_strong_guy", sql: m0 }, -] diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index c1038ab3113..9be299bac30 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -1,9 +1,11 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { Database } from "bun:sqlite" import { drizzle } from "drizzle-orm/bun-sqlite" +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 { ProjectTable } from "../../src/project/project.sql" @@ -17,7 +19,6 @@ import { PermissionTable, } from "../../src/session/session.sql" import { SessionShareTable, ShareTable } from "../../src/share/share.sql" -import { migrations } from "../../src/storage/migrations.generated" // Test fixtures const fixtures = { @@ -76,14 +77,16 @@ function createTestDb() { const sqlite = new Database(":memory:") sqlite.exec("PRAGMA foreign_keys = ON") - // Apply schema migrations - for (const migration of migrations) { - const statements = migration.sql.split("--> statement-breakpoint") - for (const stmt of statements) { - const trimmed = stmt.trim() - if (trimmed) sqlite.exec(trimmed) - } + // 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, + })) + migrate(drizzle({ client: sqlite }), migrations) return sqlite } @@ -122,7 +125,7 @@ describe("JSON to SQLite migration", () => { expect(stats?.projects).toBe(1) - const db = drizzle(sqlite) + const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) expect(projects[0].id).toBe("proj_test123abc") @@ -160,7 +163,7 @@ describe("JSON to SQLite migration", () => { await migrateFromJson(sqlite, storageDir) - const db = drizzle(sqlite) + const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) expect(sessions[0].id).toBe("ses_test456def") @@ -200,7 +203,7 @@ describe("JSON to SQLite migration", () => { expect(stats?.messages).toBe(1) expect(stats?.parts).toBe(1) - const db = drizzle(sqlite) + const db = drizzle({ client: sqlite }) const messages = db.select().from(MessageTable).all() expect(messages.length).toBe(1) expect(messages[0].data.id).toBe("msg_test789ghi") @@ -272,7 +275,7 @@ describe("JSON to SQLite migration", () => { await migrateFromJson(sqlite, storageDir) - const db = drizzle(sqlite) + const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing }) From 57edb0ddc5fe6e1bc64e7b318add274335af4771 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 26 Jan 2026 08:44:19 -0500 Subject: [PATCH 05/17] sync --- AGENTS.md | 26 ++- packages/opencode/src/cli/cmd/database.ts | 16 +- packages/opencode/src/cli/cmd/import.ts | 8 +- packages/opencode/src/permission/next.ts | 2 +- packages/opencode/src/project/project.sql.ts | 20 +- packages/opencode/src/project/project.ts | 4 +- .../opencode/src/server/routes/session.ts | 19 +- packages/opencode/src/session/index.ts | 209 +++++++++++++----- packages/opencode/src/session/message-v2.ts | 8 +- packages/opencode/src/session/prompt.ts | 31 +-- packages/opencode/src/session/revert.ts | 22 +- packages/opencode/src/session/session.sql.ts | 76 +++---- packages/opencode/src/session/summary.ts | 13 +- packages/opencode/src/session/todo.ts | 6 +- packages/opencode/src/share/share-next.ts | 8 +- packages/opencode/src/share/share.sql.ts | 8 +- packages/opencode/src/storage/db.ts | 87 ++++---- .../opencode/src/storage/json-migration.ts | 26 +-- .../test/storage/json-migration.test.ts | 2 +- 19 files changed, 340 insertions(+), 251 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6119e653e1d..750aeff1de6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,16 +19,16 @@ ### Naming -Prefer single word variable names. Only use multiple words if necessary. +Prefer single word names for variables and functions. Only use multiple words if necessary. ```ts // Good const foo = 1 -const bar = 2 +function journal(dir: string) {} // Bad const fooBar = 1 -const barBaz = 2 +function prepareJournal(dir: string) {} ``` Reduce total variable count by inlining when a value is only used once. @@ -87,6 +87,26 @@ function foo() { } ``` +### Schema Definitions (Drizzle) + +Use snake_case for field names so column names don't need to be redefined as strings. + +```ts +// Good +const table = sqliteTable("session", { + id: text().primaryKey(), + project_id: text().notNull(), + created_at: integer().notNull(), +}) + +// Bad +const table = sqliteTable("session", { + id: text("id").primaryKey(), + projectID: text("project_id").notNull(), + createdAt: integer("created_at").notNull(), +}) +``` + ## Testing - Avoid mocks as much as possible diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts index 1fda74cf6b4..44e72950537 100644 --- a/packages/opencode/src/cli/cmd/database.ts +++ b/packages/opencode/src/cli/cmd/database.ts @@ -65,7 +65,7 @@ const ExportCommand = cmd({ // 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.projectID) + 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++ @@ -74,7 +74,7 @@ const ExportCommand = cmd({ // 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.sessionID) + 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++ @@ -83,7 +83,7 @@ const ExportCommand = cmd({ // 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.messageID) + 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++ @@ -93,7 +93,7 @@ const ExportCommand = cmd({ 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.sessionID}.json`), JSON.stringify(row.data, null, 2)) + await Bun.write(path.join(diffDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) stats.diffs++ } @@ -101,7 +101,7 @@ const ExportCommand = cmd({ 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.sessionID}.json`), JSON.stringify(row.data, null, 2)) + await Bun.write(path.join(todoDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) stats.todos++ } @@ -109,7 +109,7 @@ const ExportCommand = cmd({ 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.projectID}.json`), JSON.stringify(row.data, null, 2)) + await Bun.write(path.join(permDir, `${row.project_id}.json`), JSON.stringify(row.data, null, 2)) stats.permissions++ } @@ -117,7 +117,7 @@ const ExportCommand = cmd({ 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.sessionID}.json`), JSON.stringify(row.data, null, 2)) + await Bun.write(path.join(sessionShareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) stats.sessionShares++ } @@ -125,7 +125,7 @@ const ExportCommand = cmd({ 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.sessionID}.json`), JSON.stringify(row.data, null, 2)) + await Bun.write(path.join(shareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) stats.shares++ } diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 7f97b70c317..5c0d9bdad57 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -90,8 +90,8 @@ export const ImportCommand = cmd({ .insert(MessageTable) .values({ id: msg.info.id, - sessionID: exportData.info.id, - createdAt: msg.info.time?.created ?? Date.now(), + session_id: exportData.info.id, + created_at: msg.info.time?.created ?? Date.now(), data: msg.info, }) .onConflictDoNothing() @@ -104,8 +104,8 @@ export const ImportCommand = cmd({ .insert(PartTable) .values({ id: part.id, - messageID: msg.info.id, - sessionID: exportData.info.id, + message_id: msg.info.id, + session_id: exportData.info.id, data: part, }) .onConflictDoNothing() diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 98840867b2f..1e1df62a3ce 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -109,7 +109,7 @@ export namespace PermissionNext { const state = Instance.state(() => { const projectID = Instance.project.id const row = Database.use((db) => - db.select().from(PermissionTable).where(eq(PermissionTable.projectID, projectID)).get(), + db.select().from(PermissionTable).where(eq(PermissionTable.project_id, projectID)).get(), ) const stored = row?.data ?? ([] as Ruleset) diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 651d537cf2b..0f5a856e516 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,14 +1,14 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const ProjectTable = sqliteTable("project", { - id: text("id").primaryKey(), - worktree: text("worktree").notNull(), - vcs: text("vcs"), - name: text("name"), - icon_url: text("icon_url"), - icon_color: text("icon_color"), - time_created: integer("time_created").notNull(), - time_updated: integer("time_updated").notNull(), - time_initialized: integer("time_initialized"), - sandboxes: text("sandboxes", { mode: "json" }).notNull().$type(), + id: text().primaryKey(), + worktree: text().notNull(), + vcs: text(), + name: text(), + icon_url: text(), + icon_color: text(), + time_created: integer().notNull(), + time_updated: integer().notNull(), + time_initialized: integer(), + sandboxes: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index fc940b95884..715b4f32309 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -299,7 +299,7 @@ export namespace Project { if (!globalRow) return const globalSessions = Database.use((db) => - db.select().from(SessionTable).where(eq(SessionTable.projectID, "global")).all(), + db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(), ) if (globalSessions.length === 0) return @@ -311,7 +311,7 @@ export namespace Project { log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID }) Database.use((db) => - db.update(SessionTable).set({ projectID: newProjectID }).where(eq(SessionTable.id, row.id)).run(), + db.update(SessionTable).set({ project_id: newProjectID }).where(eq(SessionTable.id, row.id)).run(), ) }).catch((error) => { log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID }) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 3850376bdb4..de1ebffe703 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -276,18 +276,15 @@ export const SessionRoutes = lazy(() => const sessionID = c.req.valid("param").sessionID const updates = c.req.valid("json") - const updatedSession = await Session.update( - sessionID, - (session) => { - if (updates.title !== undefined) { - session.title = updates.title - } - if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived - }, - { touch: false }, - ) + let session = await Session.get(sessionID) + if (updates.title !== undefined) { + session = await Session.setTitle({ sessionID, title: updates.title }) + } + if (updates.time?.archived !== undefined) { + session = await Session.setArchived({ sessionID, time: updates.time.archived }) + } - return c.json(updatedSession) + return c.json(session) }, ) .post( diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index db06483eea2..5ad143c93ec 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -55,10 +55,10 @@ export namespace Session { : undefined const share = row.share_url ? { url: row.share_url } : undefined const revert = - row.revert_messageID !== null + row.revert_message_id !== null ? { - messageID: row.revert_messageID, - partID: row.revert_partID ?? undefined, + messageID: row.revert_message_id, + partID: row.revert_part_id ?? undefined, snapshot: row.revert_snapshot ?? undefined, diff: row.revert_diff ?? undefined, } @@ -66,9 +66,9 @@ export namespace Session { return { id: row.id, slug: row.slug, - projectID: row.projectID, + projectID: row.project_id, directory: row.directory, - parentID: row.parentID ?? undefined, + parentID: row.parent_id ?? undefined, title: row.title, version: row.version, summary, @@ -87,8 +87,8 @@ export namespace Session { export function toRow(info: Info) { return { id: info.id, - projectID: info.projectID, - parentID: info.parentID, + project_id: info.projectID, + parent_id: info.parentID, slug: info.slug, directory: info.directory, title: info.title, @@ -98,8 +98,8 @@ export namespace Session { summary_deletions: info.summary?.deletions, summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, - revert_messageID: info.revert?.messageID ?? null, - revert_partID: info.revert?.partID ?? null, + revert_message_id: info.revert?.messageID ?? null, + revert_part_id: info.revert?.partID ?? null, revert_snapshot: info.revert?.snapshot ?? null, revert_diff: info.revert?.diff ?? null, permission: info.permission, @@ -255,9 +255,10 @@ export namespace Session { ) export const touch = fn(Identifier.schema("session"), async (sessionID) => { - await update(sessionID, (draft) => { - draft.time.updated = Date.now() - }) + const now = Date.now() + Database.use((db) => db.update(SessionTable).set({ time_updated: now }).where(eq(SessionTable.id, sessionID)).run()) + const info = await get(sessionID) + Bus.publish(Event.Updated, { info }) }) export async function createNext(input: { @@ -288,15 +289,9 @@ export namespace Session { }) const cfg = await Config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) - share(result.id) - .then((share) => { - update(result.id, (draft) => { - draft.share = share - }) - }) - .catch(() => { - // Silently ignore sharing errors during session creation - }) + share(result.id).catch(() => { + // Silently ignore sharing errors during session creation + }) Bus.publish(Event.Updated, { info: result, }) @@ -317,7 +312,7 @@ export namespace Session { }) export const getShare = fn(Identifier.schema("session"), async (id) => { - const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.sessionID, id)).get()) + const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.session_id, id)).get()) return row?.data }) @@ -328,15 +323,9 @@ export namespace Session { } const { ShareNext } = await import("@/share/share-next") const share = await ShareNext.create(id) - await update( - id, - (draft) => { - draft.share = { - url: share.url, - } - }, - { touch: false }, - ) + Database.use((db) => db.update(SessionTable).set({ share_url: share.url }).where(eq(SessionTable.id, id)).run()) + const info = await get(id) + Bus.publish(Event.Updated, { info }) return share }) @@ -344,33 +333,135 @@ export namespace Session { // Use ShareNext to remove the share (same as share function uses ShareNext to create) const { ShareNext } = await import("@/share/share-next") await ShareNext.remove(id) - await update( - id, - (draft) => { - draft.share = undefined - }, - { touch: false }, + Database.use((db) => db.update(SessionTable).set({ share_url: null }).where(eq(SessionTable.id, id)).run()) + const info = await get(id) + Bus.publish(Event.Updated, { info }) + }) + + export const setTitle = fn( + z.object({ + sessionID: Identifier.schema("session"), + title: z.string(), + }), + async (input) => { + Database.use((db) => + db.update(SessionTable).set({ title: input.title }).where(eq(SessionTable.id, input.sessionID)).run(), + ) + const info = await get(input.sessionID) + Bus.publish(Event.Updated, { info }) + return info + }, + ) + + export const setArchived = fn( + z.object({ + sessionID: Identifier.schema("session"), + time: z.number().optional(), + }), + async (input) => { + Database.use((db) => + db.update(SessionTable).set({ time_archived: input.time }).where(eq(SessionTable.id, input.sessionID)).run(), + ) + const info = await get(input.sessionID) + Bus.publish(Event.Updated, { info }) + return info + }, + ) + + export const setPermission = fn( + z.object({ + sessionID: Identifier.schema("session"), + permission: PermissionNext.Ruleset, + }), + async (input) => { + Database.use((db) => + db + .update(SessionTable) + .set({ permission: input.permission, time_updated: Date.now() }) + .where(eq(SessionTable.id, input.sessionID)) + .run(), + ) + const info = await get(input.sessionID) + Bus.publish(Event.Updated, { info }) + return info + }, + ) + + export const setRevert = fn( + z.object({ + sessionID: Identifier.schema("session"), + revert: Info.shape.revert, + summary: Info.shape.summary, + }), + async (input) => { + Database.use((db) => + db + .update(SessionTable) + .set({ + revert_message_id: input.revert?.messageID ?? null, + revert_part_id: input.revert?.partID ?? null, + revert_snapshot: input.revert?.snapshot ?? null, + revert_diff: input.revert?.diff ?? null, + summary_additions: input.summary?.additions, + summary_deletions: input.summary?.deletions, + summary_files: input.summary?.files, + time_updated: Date.now(), + }) + .where(eq(SessionTable.id, input.sessionID)) + .run(), + ) + const info = await get(input.sessionID) + Bus.publish(Event.Updated, { info }) + return info + }, + ) + + export const clearRevert = fn(Identifier.schema("session"), async (sessionID) => { + Database.use((db) => + db + .update(SessionTable) + .set({ + revert_message_id: null, + revert_part_id: null, + revert_snapshot: null, + revert_diff: null, + time_updated: Date.now(), + }) + .where(eq(SessionTable.id, sessionID)) + .run(), ) + const info = await get(sessionID) + Bus.publish(Event.Updated, { info }) + return info }) - export function update(id: string, editor: (session: Info) => void, options?: { touch?: boolean }) { - const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - if (!row) throw new Error(`Session not found: ${id}`) - const data = fromRow(row) - editor(data) - if (options?.touch !== false) { - data.time.updated = Date.now() - } - Database.use((db) => db.update(SessionTable).set(toRow(data)).where(eq(SessionTable.id, id)).run()) - Bus.publish(Event.Updated, { - info: data, - }) - return data - } + export const setSummary = fn( + z.object({ + sessionID: Identifier.schema("session"), + summary: Info.shape.summary, + }), + async (input) => { + Database.use((db) => + db + .update(SessionTable) + .set({ + summary_additions: input.summary?.additions, + summary_deletions: input.summary?.deletions, + summary_files: input.summary?.files, + time_updated: Date.now(), + }) + .where(eq(SessionTable.id, input.sessionID)) + .run(), + ) + const info = await get(input.sessionID) + Bus.publish(Event.Updated, { info }) + return info + }, + ) export const diff = fn(Identifier.schema("session"), async (sessionID) => { const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, sessionID)).get(), + db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, sessionID)).get(), ) return row?.data ?? [] }) @@ -394,7 +485,7 @@ export namespace Session { export function* list() { const project = Instance.project const rows = Database.use((db) => - db.select().from(SessionTable).where(eq(SessionTable.projectID, project.id)).all(), + db.select().from(SessionTable).where(eq(SessionTable.project_id, project.id)).all(), ) for (const row of rows) { yield fromRow(row) @@ -402,7 +493,7 @@ export namespace Session { } export const children = fn(Identifier.schema("session"), async (parentID) => { - const rows = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.parentID, parentID)).all()) + const rows = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.parent_id, parentID)).all()) return rows.map((row) => fromRow(row)) }) @@ -425,14 +516,14 @@ export namespace Session { }) export const updateMessage = fn(MessageV2.Info, async (msg) => { - const createdAt = msg.role === "user" ? msg.time.created : msg.time.created + const created_at = msg.role === "user" ? msg.time.created : msg.time.created Database.use((db) => db .insert(MessageTable) .values({ id: msg.id, - sessionID: msg.sessionID, - createdAt, + session_id: msg.sessionID, + created_at, data: msg, }) .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) @@ -497,8 +588,8 @@ export namespace Session { .insert(PartTable) .values({ id: part.id, - messageID: part.messageID, - sessionID: part.sessionID, + message_id: part.messageID, + session_id: part.sessionID, data: part, }) .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index e92252400c1..6e38edbad6f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -612,8 +612,8 @@ export namespace MessageV2 { db .select() .from(MessageTable) - .where(eq(MessageTable.sessionID, sessionID)) - .orderBy(desc(MessageTable.createdAt)) + .where(eq(MessageTable.session_id, sessionID)) + .orderBy(desc(MessageTable.created_at)) .all(), ) for (const row of rows) { @@ -624,8 +624,8 @@ export namespace MessageV2 { } }) - export const parts = fn(Identifier.schema("message"), async (messageID) => { - const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.messageID, messageID)).all()) + export const parts = fn(Identifier.schema("message"), async (message_id) => { + const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).all()) const result = rows.map((row) => row.data) result.sort((a, b) => (a.id > b.id ? 1 : -1)) return result diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8554b44a727..fc10d7c780c 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -167,9 +167,7 @@ export namespace SessionPrompt { } if (permissions.length > 0) { session.permission = permissions - await Session.update(session.id, (draft) => { - draft.permission = permissions - }) + await Session.setPermission({ sessionID: session.id, permission: permissions }) } if (input.noReply === true) { @@ -1795,21 +1793,16 @@ NOTE: At any point in time through this workflow you should feel free to ask the ], }) const text = await result.text.catch((err) => log.error("failed to generate title", { error: err })) - if (text) - return Session.update( - input.session.id, - (draft) => { - const cleaned = text - .replace(/[\s\S]*?<\/think>\s*/g, "") - .split("\n") - .map((line) => line.trim()) - .find((line) => line.length > 0) - if (!cleaned) return - - const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - draft.title = title - }, - { touch: false }, - ) + if (text) { + const cleaned = text + .replace(/[\s\S]*?<\/think>\s*/g, "") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!cleaned) return + + const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned + return Session.setTitle({ sessionID: input.session.id, title }) + } } } diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 6a27fc7b9a4..a4356dc70a7 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -64,21 +64,22 @@ export namespace SessionRevert { Database.use((db) => db .insert(SessionDiffTable) - .values({ sessionID: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .values({ session_id: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) .run(), ) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, }) - return Session.update(input.sessionID, (draft) => { - draft.revert = revert - draft.summary = { + return Session.setRevert({ + sessionID: input.sessionID, + revert, + summary: { additions: diffs.reduce((sum, x) => sum + x.additions, 0), deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), files: diffs.length, - } + }, }) } return session @@ -90,10 +91,7 @@ export namespace SessionRevert { const session = await Session.get(input.sessionID) if (!session.revert) return session if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot) - const next = await Session.update(input.sessionID, (draft) => { - draft.revert = undefined - }) - return next + return Session.clearRevert(input.sessionID) } export async function cleanup(session: Session.Info) { @@ -121,8 +119,6 @@ export namespace SessionRevert { }) } } - await Session.update(sessionID, (draft) => { - draft.revert = undefined - }) + await Session.clearRevert(sessionID) } } diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index be35dd17033..7d52d87a4b0 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -8,76 +8,76 @@ import type { PermissionNext } from "@/permission/next" export const SessionTable = sqliteTable( "session", { - id: text("id").primaryKey(), - projectID: text("project_id") + id: text().primaryKey(), + project_id: text() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), - parentID: text("parent_id"), - slug: text("slug").notNull(), - directory: text("directory").notNull(), - title: text("title").notNull(), - version: text("version").notNull(), - share_url: text("share_url"), - summary_additions: integer("summary_additions"), - summary_deletions: integer("summary_deletions"), - summary_files: integer("summary_files"), - summary_diffs: text("summary_diffs", { mode: "json" }).$type(), - revert_messageID: text("revert_message_id"), - revert_partID: text("revert_part_id"), - revert_snapshot: text("revert_snapshot"), - revert_diff: text("revert_diff"), - permission: text("permission", { mode: "json" }).$type(), - time_created: integer("time_created").notNull(), - time_updated: integer("time_updated").notNull(), - time_compacting: integer("time_compacting"), - time_archived: integer("time_archived"), + parent_id: text(), + slug: text().notNull(), + directory: text().notNull(), + title: text().notNull(), + version: text().notNull(), + share_url: text(), + summary_additions: integer(), + summary_deletions: integer(), + summary_files: integer(), + summary_diffs: text({ mode: "json" }).$type(), + revert_message_id: text(), + revert_part_id: text(), + revert_snapshot: text(), + revert_diff: text(), + permission: text({ mode: "json" }).$type(), + time_created: integer().notNull(), + time_updated: integer().notNull(), + time_compacting: integer(), + time_archived: integer(), }, - (table) => [index("session_project_idx").on(table.projectID), index("session_parent_idx").on(table.parentID)], + (table) => [index("session_project_idx").on(table.project_id), index("session_parent_idx").on(table.parent_id)], ) export const MessageTable = sqliteTable( "message", { - id: text("id").primaryKey(), - sessionID: text("session_id") + id: text().primaryKey(), + session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - createdAt: integer("created_at").notNull(), - data: text("data", { mode: "json" }).notNull().$type(), + created_at: integer().notNull(), + data: text({ mode: "json" }).notNull().$type(), }, - (table) => [index("message_session_idx").on(table.sessionID)], + (table) => [index("message_session_idx").on(table.session_id)], ) export const PartTable = sqliteTable( "part", { - id: text("id").primaryKey(), - messageID: text("message_id") + id: text().primaryKey(), + message_id: text() .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), - sessionID: text("session_id").notNull(), - data: text("data", { mode: "json" }).notNull().$type(), + session_id: text().notNull(), + data: text({ mode: "json" }).notNull().$type(), }, - (table) => [index("part_message_idx").on(table.messageID), index("part_session_idx").on(table.sessionID)], + (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], ) export const SessionDiffTable = sqliteTable("session_diff", { - sessionID: text("session_id") + session_id: text() .primaryKey() .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text("data", { mode: "json" }).notNull().$type(), + data: text({ mode: "json" }).notNull().$type(), }) export const TodoTable = sqliteTable("todo", { - sessionID: text("session_id") + session_id: text() .primaryKey() .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text("data", { mode: "json" }).notNull().$type(), + data: text({ mode: "json" }).notNull().$type(), }) export const PermissionTable = sqliteTable("permission", { - projectID: text("project_id") + project_id: text() .primaryKey() .references(() => ProjectTable.id, { onDelete: "cascade" }), - data: text("data", { mode: "json" }).notNull().$type(), + data: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 2472eee7a39..0183a9d3dff 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -48,18 +48,19 @@ export namespace SessionSummary { return files.has(x.file) }), ) - await Session.update(input.sessionID, (draft) => { - draft.summary = { + await Session.setSummary({ + sessionID: input.sessionID, + summary: { additions: diffs.reduce((sum, x) => sum + x.additions, 0), deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), files: diffs.length, - } + }, }) Database.use((db) => db .insert(SessionDiffTable) - .values({ sessionID: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } }) + .values({ session_id: input.sessionID, data: diffs }) + .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) .run(), ) Bus.publish(Session.Event.Diff, { @@ -124,7 +125,7 @@ export namespace SessionSummary { }), async (input) => { const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.sessionID, input.sessionID)).get(), + db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, input.sessionID)).get(), ) return row?.data ?? [] }, diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 8ba5a0281cc..03bbcc148ee 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -29,15 +29,15 @@ export namespace Todo { Database.use((db) => db .insert(TodoTable) - .values({ sessionID: input.sessionID, data: input.todos }) - .onConflictDoUpdate({ target: TodoTable.sessionID, set: { data: input.todos } }) + .values({ session_id: input.sessionID, data: input.todos }) + .onConflictDoUpdate({ target: TodoTable.session_id, set: { data: input.todos } }) .run(), ) Bus.publish(Event.Updated, input) } export function get(sessionID: string) { - const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.sessionID, sessionID)).get()) + const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).get()) return row?.data ?? [] } } diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 0f18cb974db..0cf978930e1 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -81,8 +81,8 @@ export namespace ShareNext { Database.use((db) => db .insert(SessionShareTable) - .values({ sessionID, data: result }) - .onConflictDoUpdate({ target: SessionShareTable.sessionID, set: { data: result } }) + .values({ session_id: sessionID, data: result }) + .onConflictDoUpdate({ target: SessionShareTable.session_id, set: { data: result } }) .run(), ) fullSync(sessionID) @@ -91,7 +91,7 @@ export namespace ShareNext { function get(sessionID: string) { const row = Database.use((db) => - db.select().from(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).get(), + db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), ) return row?.data } @@ -169,7 +169,7 @@ export namespace ShareNext { secret: share.secret, }), }) - Database.use((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.sessionID, sessionID)).run()) + Database.use((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) } async function fullSync(sessionID: string) { diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index 7a65fd764bd..bf8a1904617 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -3,10 +3,10 @@ import { SessionTable } from "../session/session.sql" import type { Session } from "../session" export const SessionShareTable = sqliteTable("session_share", { - sessionID: text("session_id") + session_id: text() .primaryKey() .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text("data", { mode: "json" }).notNull().$type<{ + data: text({ mode: "json" }).notNull().$type<{ id: string secret: string url: string @@ -14,6 +14,6 @@ export const SessionShareTable = sqliteTable("session_share", { }) export const ShareTable = sqliteTable("share", { - sessionID: text("session_id").primaryKey(), - data: text("data", { mode: "json" }).notNull().$type(), + session_id: text().primaryKey(), + data: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 824bc856584..1f6cc308072 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,6 +1,6 @@ import { Database as BunDatabase } from "bun:sqlite" import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" -import { migrate as drizzleMigrate } from "drizzle-orm/bun-sqlite/migrator" +import { migrate } from "drizzle-orm/bun-sqlite/migrator" import type { SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { Context } from "../util/context" @@ -29,6 +29,22 @@ export namespace Database { type Client = SQLiteBunDatabase + 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, + })) + } + const client = lazy(() => { log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) @@ -41,11 +57,22 @@ export namespace Database { sqlite.run("PRAGMA foreign_keys = ON") const db = drizzle({ client: sqlite }) - migrate(db) + + // Apply schema migrations + const entries = + typeof OPENCODE_MIGRATIONS !== "undefined" + ? OPENCODE_MIGRATIONS + : journal(path.join(import.meta.dirname, "../../migration")) + if (entries.length > 0) { + log.info("applying migrations", { + count: entries.length, + mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", + }) + migrate(db, entries) + } // Run json migration if not already done - const marker = sqlite.prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = 'json-migration'").get() - if (!marker) { + 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) => { @@ -62,19 +89,18 @@ export namespace Database { export type TxOrDb = Transaction | Client - const TransactionContext = Context.create<{ + const ctx = Context.create<{ tx: TxOrDb effects: (() => void | Promise)[] }>("database") export function use(callback: (trx: TxOrDb) => T): T { try { - const { tx } = TransactionContext.use() - return callback(tx) + return callback(ctx.use().tx) } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const result = TransactionContext.provide({ effects, tx: client() }, () => callback(client())) + const result = ctx.provide({ effects, tx: client() }, () => callback(client())) for (const effect of effects) effect() return result } @@ -82,24 +108,22 @@ export namespace Database { } } - export function effect(effect: () => void | Promise) { + export function effect(fn: () => void | Promise) { try { - const { effects } = TransactionContext.use() - effects.push(effect) + ctx.use().effects.push(fn) } catch { - effect() + fn() } } export function transaction(callback: (tx: TxOrDb) => T): T { try { - const { tx } = TransactionContext.use() - return callback(tx) + return callback(ctx.use().tx) } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] const result = client().transaction((tx) => { - return TransactionContext.provide({ tx, effects }, () => callback(tx)) + return ctx.provide({ tx, effects }, () => callback(tx)) }) for (const effect of effects) effect() return result @@ -108,36 +132,3 @@ export namespace Database { } } } - -type MigrationsJournal = { sql: string; timestamp: number }[] - -function prepareJournal(dir: string): MigrationsJournal { - const file = path.join(dir, "meta/_journal.json") - if (!Bun.file(file).size) return [] - - const journal = JSON.parse(readFileSync(file, "utf-8")) as { - entries: { tag: string; when: number }[] - } - - return journal.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) -} - -function migrate(db: SQLiteBunDatabase) { - const journal = - typeof OPENCODE_MIGRATIONS !== "undefined" - ? OPENCODE_MIGRATIONS - : prepareJournal(path.join(import.meta.dirname, "../../migration")) - - if (journal.length === 0) { - log.info("no migrations found") - return - } - log.info("applying migrations", { - count: journal.length, - mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", - }) - drizzleMigrate(db, journal) -} diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index df15ff6df04..4936387fd7c 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -84,8 +84,8 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin db.insert(SessionTable) .values({ id: data.id, - projectID: data.projectID, - parentID: data.parentID ?? null, + project_id: data.projectID, + parent_id: data.parentID ?? null, slug: data.slug ?? "", directory: data.directory ?? "", title: data.title ?? "", @@ -95,8 +95,8 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin summary_deletions: data.summary?.deletions ?? null, summary_files: data.summary?.files ?? null, summary_diffs: data.summary?.diffs ?? null, - revert_messageID: data.revert?.messageID ?? null, - revert_partID: data.revert?.partID ?? null, + revert_message_id: data.revert?.messageID ?? null, + revert_part_id: data.revert?.partID ?? null, revert_snapshot: data.revert?.snapshot ?? null, revert_diff: data.revert?.diff ?? null, permission: data.permission ?? null, @@ -132,8 +132,8 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin db.insert(MessageTable) .values({ id: data.id, - sessionID: data.sessionID, - createdAt: data.time?.created ?? Date.now(), + session_id: data.sessionID, + created_at: data.time?.created ?? Date.now(), data, }) .onConflictDoNothing() @@ -163,8 +163,8 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin db.insert(PartTable) .values({ id: data.id, - messageID: data.messageID, - sessionID: data.sessionID, + message_id: data.messageID, + session_id: data.sessionID, data, }) .onConflictDoNothing() @@ -188,7 +188,7 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin log.warn("skipping orphaned session_diff", { sessionID }) continue } - db.insert(SessionDiffTable).values({ sessionID, data }).onConflictDoNothing().run() + db.insert(SessionDiffTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() stats.diffs++ } catch (e) { stats.errors.push(`failed to migrate session_diff ${file}: ${e}`) @@ -207,7 +207,7 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin log.warn("skipping orphaned todo", { sessionID }) continue } - db.insert(TodoTable).values({ sessionID, data }).onConflictDoNothing().run() + db.insert(TodoTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() stats.todos++ } catch (e) { stats.errors.push(`failed to migrate todo ${file}: ${e}`) @@ -226,7 +226,7 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin log.warn("skipping orphaned permission", { projectID }) continue } - db.insert(PermissionTable).values({ projectID, data }).onConflictDoNothing().run() + db.insert(PermissionTable).values({ project_id: projectID, data }).onConflictDoNothing().run() stats.permissions++ } catch (e) { stats.errors.push(`failed to migrate permission ${file}: ${e}`) @@ -245,7 +245,7 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin log.warn("skipping orphaned session_share", { sessionID }) continue } - db.insert(SessionShareTable).values({ sessionID, data }).onConflictDoNothing().run() + db.insert(SessionShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() stats.shares++ } catch (e) { stats.errors.push(`failed to migrate session_share ${file}: ${e}`) @@ -259,7 +259,7 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin try { const data = await Bun.file(file).json() const sessionID = path.basename(file, ".json") - db.insert(ShareTable).values({ sessionID, data }).onConflictDoNothing().run() + db.insert(ShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() } catch (e) { stats.errors.push(`failed to migrate share ${file}: ${e}`) } diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 9be299bac30..b9981858ba8 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -167,7 +167,7 @@ describe("JSON to SQLite migration", () => { const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) expect(sessions[0].id).toBe("ses_test456def") - expect(sessions[0].projectID).toBe("proj_test123abc") + expect(sessions[0].project_id).toBe("proj_test123abc") expect(sessions[0].slug).toBe("test-session") expect(sessions[0].title).toBe("Test Session Title") expect(sessions[0].summary_additions).toBe(10) From bf0754caeb880c500e1b24d10162e8d44e47888b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 26 Jan 2026 10:35:53 -0500 Subject: [PATCH 06/17] sync --- .../src/cli/cmd/tui/routes/session/index.tsx | 4 +-- .../test/storage/json-migration.test.ts | 29 ------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c73a520428b..a3ea493cc21 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1952,8 +1952,8 @@ function ApplyPatch(props: ToolProps) { - - apply_patch + + Patch diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index b9981858ba8..70582eed045 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -232,31 +232,6 @@ describe("JSON to SQLite migration", () => { expect(stats?.sessions).toBe(0) }) - test("creates sqlite-migrated marker file", async () => { - await migrateFromJson(sqlite, storageDir) - - const marker = path.join(storageDir, "sqlite-migrated") - expect(await Bun.file(marker).exists()).toBe(true) - }) - - test("skips if already migrated", async () => { - await Bun.write(path.join(storageDir, "sqlite-migrated"), Date.now().toString()) - await Bun.write( - path.join(storageDir, "project", "proj_test123abc.json"), - JSON.stringify({ - id: "proj_test123abc", - worktree: "/", - time: { created: Date.now(), updated: Date.now() }, - sandboxes: [], - }), - ) - - const stats = await migrateFromJson(sqlite, storageDir) - - // Should return undefined (skipped) since already migrated - expect(stats).toBeUndefined() - }) - test("is idempotent (running twice doesn't duplicate)", async () => { await Bun.write( path.join(storageDir, "project", "proj_test123abc.json"), @@ -269,10 +244,6 @@ describe("JSON to SQLite migration", () => { ) await migrateFromJson(sqlite, storageDir) - - // Remove marker to run again - await fs.rm(path.join(storageDir, "sqlite-migrated")) - await migrateFromJson(sqlite, storageDir) const db = drizzle({ client: sqlite }) From acdcf7fa88d3c2645359d6fe7ba0dd4338f6b58f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 26 Jan 2026 11:11:09 -0500 Subject: [PATCH 07/17] core: remove dependency on remeda to simplify dependencies --- packages/opencode/src/project/project.ts | 185 ++++++++++++----------- packages/opencode/src/session/revert.ts | 49 ++++-- 2 files changed, 127 insertions(+), 107 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 715b4f32309..964ff11c026 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -1,5 +1,4 @@ import z from "zod" -import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import path from "path" import { $ } from "bun" @@ -13,7 +12,6 @@ import { fn } from "@opencode-ai/util/fn" import { BusEvent } from "@/bus/bus-event" import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" -import { existsSync } from "fs" export namespace Project { const log = Log.create({ service: "project" }) @@ -61,7 +59,7 @@ export namespace Project { return { id: row.id, worktree: row.worktree, - vcs: row.vcs as Info["vcs"], + vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, name: row.name ?? undefined, icon, time: { @@ -76,61 +74,58 @@ export namespace Project { export async function fromDirectory(directory: string) { log.info("fromDirectory", { directory }) - const { id, sandbox, worktree, vcs } = await iife(async () => { + const data = await iife(async () => { const matches = Filesystem.up({ targets: [".git"], start: directory }) const git = await matches.next().then((x) => x.value) await matches.return() if (git) { - let sandbox = path.dirname(git) + const sandbox = path.dirname(git) + const bin = Bun.which("git") - const gitBinary = Bun.which("git") - - // cached id calculation - let id = await Bun.file(path.join(git, "opencode")) + const cached = await Bun.file(path.join(git, "opencode")) .text() .then((x) => x.trim()) .catch(() => undefined) - if (!gitBinary) { + if (!bin) { return { - id: id ?? "global", + id: cached ?? "global", worktree: sandbox, sandbox: sandbox, vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), } } - // generate id from root commit - if (!id) { - const roots = await $`git rev-list --max-parents=0 --all` - .quiet() - .nothrow() - .cwd(sandbox) - .text() - .then((x) => - x - .split("\n") - .filter(Boolean) - .map((x) => x.trim()) - .toSorted(), - ) - .catch(() => undefined) + const roots = cached + ? undefined + : await $`git rev-list --max-parents=0 --all` + .quiet() + .nothrow() + .cwd(sandbox) + .text() + .then((x) => + x + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted(), + ) + .catch(() => undefined) - if (!roots) { - return { - id: "global", - worktree: sandbox, - sandbox: sandbox, - vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), - } + if (!cached && !roots) { + return { + id: "global", + worktree: sandbox, + sandbox: sandbox, + vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), } + } - id = roots[0] - if (id) { - void Bun.file(path.join(git, "opencode")) - .write(id) - .catch(() => undefined) - } + const id = cached ?? roots?.[0] + if (!cached && id) { + void Bun.file(path.join(git, "opencode")) + .write(id) + .catch(() => undefined) } if (!id) { @@ -159,33 +154,31 @@ export namespace Project { } } - sandbox = top - - const worktree = await $`git rev-parse --git-common-dir` + const tree = await $`git rev-parse --git-common-dir` .quiet() .nothrow() - .cwd(sandbox) + .cwd(top) .text() .then((x) => { const dirname = path.dirname(x.trim()) - if (dirname === ".") return sandbox + if (dirname === ".") return top return dirname }) .catch(() => undefined) - if (!worktree) { + if (!tree) { return { id, - sandbox, - worktree: sandbox, + sandbox: top, + worktree: top, vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), } } return { id, - sandbox, - worktree, + sandbox: top, + worktree: tree, vcs: "git", } } @@ -198,21 +191,21 @@ export namespace Project { } }) - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get()) const existing = await iife(async () => { if (row) return fromRow(row) const fresh: Info = { - id, - worktree, - vcs: vcs as Info["vcs"], + id: data.id, + worktree: data.worktree, + vcs: data.vcs as Info["vcs"], sandboxes: [], time: { created: Date.now(), updated: Date.now(), }, } - if (id !== "global") { - await migrateFromGlobal(id, worktree) + if (data.id !== "global") { + await migrateFromGlobal(data.id, data.worktree) } return fresh }) @@ -221,19 +214,27 @@ export namespace Project { const result: Info = { ...existing, - worktree, - vcs: vcs as Info["vcs"], + worktree: data.worktree, + vcs: data.vcs as Info["vcs"], time: { ...existing.time, updated: Date.now(), }, } - if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox) - result.sandboxes = result.sandboxes.filter((x) => existsSync(x)) + if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox)) + result.sandboxes.push(data.sandbox) + const sandboxes: string[] = [] + for (const x of result.sandboxes) { + const stat = await Bun.file(x) + .stat() + .catch(() => undefined) + if (stat) sandboxes.push(x) + } + result.sandboxes = sandboxes const insert = { id: result.id, worktree: result.worktree, - vcs: result.vcs, + vcs: result.vcs ?? null, name: result.name, icon_url: result.icon?.url, icon_color: result.icon?.color, @@ -244,7 +245,7 @@ export namespace Project { } const updateSet = { worktree: result.worktree, - vcs: result.vcs, + vcs: result.vcs ?? null, name: result.name, icon_url: result.icon?.url, icon_color: result.icon?.color, @@ -261,7 +262,7 @@ export namespace Project { properties: result, }, }) - return { project: result, sandbox } + return { project: result, sandbox: data.sandbox } } export async function discover(input: Info) { @@ -294,38 +295,36 @@ export namespace Project { return } - async function migrateFromGlobal(newProjectID: string, worktree: string) { - const globalRow = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get()) - if (!globalRow) return + async function migrateFromGlobal(id: string, worktree: string) { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get()) + if (!row) return - const globalSessions = Database.use((db) => + const sessions = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(), ) - if (globalSessions.length === 0) return + if (sessions.length === 0) return - log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length }) + log.info("migrating sessions from global", { newProjectID: id, worktree, count: sessions.length }) - await work(10, globalSessions, async (row) => { + await work(10, sessions, async (row) => { // Skip sessions that belong to a different directory if (row.directory && row.directory !== worktree) return - log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID }) - Database.use((db) => - db.update(SessionTable).set({ project_id: newProjectID }).where(eq(SessionTable.id, row.id)).run(), - ) + log.info("migrating session", { sessionID: row.id, from: "global", to: id }) + Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run()) }).catch((error) => { - log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID }) + log.error("failed to migrate sessions from global to project", { error, projectId: id }) }) } - export function setInitialized(projectID: string) { + export function setInitialized(id: string) { Database.use((db) => db .update(ProjectTable) .set({ time_initialized: Date.now(), }) - .where(eq(ProjectTable.id, projectID)) + .where(eq(ProjectTable.id, id)) .run(), ) } @@ -340,8 +339,8 @@ export namespace Project { ) } - export function get(projectID: string): Info | undefined { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) + export function get(id: string): Info | undefined { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) return undefined return fromRow(row) } @@ -379,32 +378,34 @@ export namespace Project { }, ) - export async function sandboxes(projectID: string) { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) + export async function sandboxes(id: string) { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) return [] const data = fromRow(row) const valid: string[] = [] for (const dir of data.sandboxes) { - const stat = await fs.stat(dir).catch(() => undefined) + const stat = await Bun.file(dir) + .stat() + .catch(() => undefined) if (stat?.isDirectory()) valid.push(dir) } return valid } - export async function addSandbox(projectID: string, directory: string) { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) - if (!row) throw new Error(`Project not found: ${projectID}`) + export async function addSandbox(id: string, directory: string) { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) throw new Error(`Project not found: ${id}`) const sandboxes = [...row.sandboxes] if (!sandboxes.includes(directory)) sandboxes.push(directory) const result = Database.use((db) => db .update(ProjectTable) .set({ sandboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, projectID)) + .where(eq(ProjectTable.id, id)) .returning() .get(), ) - if (!result) throw new Error(`Project not found: ${projectID}`) + if (!result) throw new Error(`Project not found: ${id}`) const data = fromRow(result) GlobalBus.emit("event", { payload: { @@ -415,19 +416,19 @@ export namespace Project { return data } - export async function removeSandbox(projectID: string, directory: string) { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()) - if (!row) throw new Error(`Project not found: ${projectID}`) - const sandboxes = row.sandboxes.filter((s: string) => s !== directory) + export async function removeSandbox(id: string, directory: string) { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) throw new Error(`Project not found: ${id}`) + const sandboxes = row.sandboxes.filter((s) => s !== directory) const result = Database.use((db) => db .update(ProjectTable) .set({ sandboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, projectID)) + .where(eq(ProjectTable.id, id)) .returning() .get(), ) - if (!result) throw new Error(`Project not found: ${projectID}`) + if (!result) throw new Error(`Project not found: ${id}`) const data = fromRow(result) GlobalBus.emit("event", { payload: { diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index a4356dc70a7..d402310c3e4 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -4,7 +4,6 @@ import { Snapshot } from "../snapshot" import { MessageV2 } from "./message-v2" import { Session } from "." import { Log } from "../util/log" -import { splitWhen } from "remeda" import { Database, eq } from "../storage/db" import { SessionDiffTable, MessageTable, PartTable } from "./session.sql" import { Bus } from "../bus" @@ -97,26 +96,46 @@ export namespace SessionRevert { export async function cleanup(session: Session.Info) { if (!session.revert) return const sessionID = session.id - let msgs = await Session.messages({ sessionID }) + const msgs = await Session.messages({ sessionID }) const messageID = session.revert.messageID - const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID) - msgs = preserve + const preserve = [] as MessageV2.WithParts[] + const remove = [] as MessageV2.WithParts[] + let target: MessageV2.WithParts | undefined + for (const msg of msgs) { + if (msg.info.id < messageID) { + preserve.push(msg) + continue + } + if (msg.info.id > messageID) { + remove.push(msg) + continue + } + if (session.revert.partID) { + preserve.push(msg) + target = msg + continue + } + remove.push(msg) + } for (const msg of remove) { Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run()) await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id }) } - const last = preserve.at(-1) - if (session.revert.partID && last) { + if (session.revert.partID && target) { const partID = session.revert.partID - const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID) - last.parts = preserveParts - for (const part of removeParts) { - Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, part.id)).run()) - await Bus.publish(MessageV2.Event.PartRemoved, { - sessionID: sessionID, - messageID: last.info.id, - partID: part.id, - }) + const removeStart = target.parts.findIndex((part) => part.id === partID) + if (removeStart >= 0) { + const preserveParts = target.parts.slice(0, removeStart) + const removeParts = target.parts.slice(removeStart) + target.parts = preserveParts + for (const part of removeParts) { + Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, part.id)).run()) + await Bus.publish(MessageV2.Event.PartRemoved, { + sessionID: sessionID, + messageID: target.info.id, + partID: part.id, + }) + } } } await Session.clearRevert(sessionID) From 2b05833c324bf9e227db2b1ae88b6da85e657dcc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 26 Jan 2026 12:00:44 -0500 Subject: [PATCH 08/17] core: ensure events publish reliably after database operations complete --- packages/opencode/src/session/index.ts | 210 +++++++++++++++---------- packages/opencode/src/storage/db.ts | 2 +- 2 files changed, 132 insertions(+), 80 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 5ad143c93ec..443c47a534f 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -256,9 +256,17 @@ export namespace Session { export const touch = fn(Identifier.schema("session"), async (sessionID) => { const now = Date.now() - Database.use((db) => db.update(SessionTable).set({ time_updated: now }).where(eq(SessionTable.id, sessionID)).run()) - const info = await get(sessionID) - Bus.publish(Event.Updated, { info }) + Database.use((db) => { + const row = db + .update(SessionTable) + .set({ time_updated: now }) + .where(eq(SessionTable.id, sessionID)) + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + }) }) export async function createNext(input: { @@ -283,9 +291,13 @@ export namespace Session { }, } log.info("created", result) - Database.use((db) => db.insert(SessionTable).values(toRow(result)).run()) - Bus.publish(Event.Created, { - info: result, + Database.use((db) => { + db.insert(SessionTable).values(toRow(result)).run() + Database.effect(() => + Bus.publish(Event.Created, { + info: result, + }), + ) }) const cfg = await Config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) @@ -323,9 +335,12 @@ export namespace Session { } const { ShareNext } = await import("@/share/share-next") const share = await ShareNext.create(id) - Database.use((db) => db.update(SessionTable).set({ share_url: share.url }).where(eq(SessionTable.id, id)).run()) - const info = await get(id) - Bus.publish(Event.Updated, { info }) + Database.use((db) => { + const row = db.update(SessionTable).set({ share_url: share.url }).where(eq(SessionTable.id, id)).returning().get() + if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + }) return share }) @@ -333,9 +348,12 @@ export namespace Session { // Use ShareNext to remove the share (same as share function uses ShareNext to create) const { ShareNext } = await import("@/share/share-next") await ShareNext.remove(id) - Database.use((db) => db.update(SessionTable).set({ share_url: null }).where(eq(SessionTable.id, id)).run()) - const info = await get(id) - Bus.publish(Event.Updated, { info }) + Database.use((db) => { + const row = db.update(SessionTable).set({ share_url: null }).where(eq(SessionTable.id, id)).returning().get() + if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + }) }) export const setTitle = fn( @@ -344,12 +362,18 @@ export namespace Session { title: z.string(), }), async (input) => { - Database.use((db) => - db.update(SessionTable).set({ title: input.title }).where(eq(SessionTable.id, input.sessionID)).run(), - ) - const info = await get(input.sessionID) - Bus.publish(Event.Updated, { info }) - return info + return Database.use((db) => { + const row = db + .update(SessionTable) + .set({ title: input.title }) + .where(eq(SessionTable.id, input.sessionID)) + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }, ) @@ -359,12 +383,18 @@ export namespace Session { time: z.number().optional(), }), async (input) => { - Database.use((db) => - db.update(SessionTable).set({ time_archived: input.time }).where(eq(SessionTable.id, input.sessionID)).run(), - ) - const info = await get(input.sessionID) - Bus.publish(Event.Updated, { info }) - return info + return Database.use((db) => { + const row = db + .update(SessionTable) + .set({ time_archived: input.time }) + .where(eq(SessionTable.id, input.sessionID)) + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }, ) @@ -374,16 +404,18 @@ export namespace Session { permission: PermissionNext.Ruleset, }), async (input) => { - Database.use((db) => - db + return Database.use((db) => { + const row = db .update(SessionTable) .set({ permission: input.permission, time_updated: Date.now() }) .where(eq(SessionTable.id, input.sessionID)) - .run(), - ) - const info = await get(input.sessionID) - Bus.publish(Event.Updated, { info }) - return info + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }, ) @@ -394,8 +426,8 @@ export namespace Session { summary: Info.shape.summary, }), async (input) => { - Database.use((db) => - db + return Database.use((db) => { + const row = db .update(SessionTable) .set({ revert_message_id: input.revert?.messageID ?? null, @@ -408,17 +440,19 @@ export namespace Session { time_updated: Date.now(), }) .where(eq(SessionTable.id, input.sessionID)) - .run(), - ) - const info = await get(input.sessionID) - Bus.publish(Event.Updated, { info }) - return info + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }, ) export const clearRevert = fn(Identifier.schema("session"), async (sessionID) => { - Database.use((db) => - db + return Database.use((db) => { + const row = db .update(SessionTable) .set({ revert_message_id: null, @@ -428,11 +462,13 @@ export namespace Session { time_updated: Date.now(), }) .where(eq(SessionTable.id, sessionID)) - .run(), - ) - const info = await get(sessionID) - Bus.publish(Event.Updated, { info }) - return info + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }) export const setSummary = fn( @@ -441,8 +477,8 @@ export namespace Session { summary: Info.shape.summary, }), async (input) => { - Database.use((db) => - db + return Database.use((db) => { + const row = db .update(SessionTable) .set({ summary_additions: input.summary?.additions, @@ -451,11 +487,13 @@ export namespace Session { time_updated: Date.now(), }) .where(eq(SessionTable.id, input.sessionID)) - .run(), - ) - const info = await get(input.sessionID) - Bus.publish(Event.Updated, { info }) - return info + .returning() + .get() + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + const info = fromRow(row) + Database.effect(() => Bus.publish(Event.Updated, { info })) + return info + }) }, ) @@ -506,9 +544,13 @@ export namespace Session { } await unshare(sessionID).catch(() => {}) // CASCADE delete handles messages and parts automatically - Database.use((db) => db.delete(SessionTable).where(eq(SessionTable.id, sessionID)).run()) - Bus.publish(Event.Deleted, { - info: session, + Database.use((db) => { + db.delete(SessionTable).where(eq(SessionTable.id, sessionID)).run() + Database.effect(() => + Bus.publish(Event.Deleted, { + info: session, + }), + ) }) } catch (e) { log.error(e) @@ -517,9 +559,8 @@ export namespace Session { export const updateMessage = fn(MessageV2.Info, async (msg) => { const created_at = msg.role === "user" ? msg.time.created : msg.time.created - Database.use((db) => - db - .insert(MessageTable) + Database.use((db) => { + db.insert(MessageTable) .values({ id: msg.id, session_id: msg.sessionID, @@ -527,10 +568,12 @@ export namespace Session { data: msg, }) .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) - .run(), - ) - Bus.publish(MessageV2.Event.Updated, { - info: msg, + .run() + Database.effect(() => + Bus.publish(MessageV2.Event.Updated, { + info: msg, + }), + ) }) return msg }) @@ -542,10 +585,14 @@ export namespace Session { }), async (input) => { // CASCADE delete handles parts automatically - Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run()) - Bus.publish(MessageV2.Event.Removed, { - sessionID: input.sessionID, - messageID: input.messageID, + Database.use((db) => { + db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run() + Database.effect(() => + Bus.publish(MessageV2.Event.Removed, { + sessionID: input.sessionID, + messageID: input.messageID, + }), + ) }) return input.messageID }, @@ -558,11 +605,15 @@ export namespace Session { partID: Identifier.schema("part"), }), async (input) => { - Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, input.partID)).run()) - Bus.publish(MessageV2.Event.PartRemoved, { - sessionID: input.sessionID, - messageID: input.messageID, - partID: input.partID, + Database.use((db) => { + db.delete(PartTable).where(eq(PartTable.id, input.partID)).run() + Database.effect(() => + Bus.publish(MessageV2.Event.PartRemoved, { + sessionID: input.sessionID, + messageID: input.messageID, + partID: input.partID, + }), + ) }) return input.partID }, @@ -583,9 +634,8 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (input) => { const part = "delta" in input ? input.part : input const delta = "delta" in input ? input.delta : undefined - Database.use((db) => - db - .insert(PartTable) + Database.use((db) => { + db.insert(PartTable) .values({ id: part.id, message_id: part.messageID, @@ -593,11 +643,13 @@ export namespace Session { data: part, }) .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) - .run(), - ) - Bus.publish(MessageV2.Event.PartUpdated, { - part, - delta, + .run() + Database.effect(() => + Bus.publish(MessageV2.Event.PartUpdated, { + part, + delta, + }), + ) }) return part }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 1f6cc308072..f49028a18b3 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -108,7 +108,7 @@ export namespace Database { } } - export function effect(fn: () => void | Promise) { + export function effect(fn: () => any | Promise) { try { ctx.use().effects.push(fn) } catch { From 5e1639de2bd199874e7cc449d5ebd76871b86c19 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 26 Jan 2026 12:33:18 -0500 Subject: [PATCH 09/17] core: improve conversation loading performance by batching database queries Reduces memory usage and speeds up conversation loading by using pagination and inArray queries instead of loading all messages at once --- packages/opencode/src/session/message-v2.ts | 63 +++++++++++++++------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 6e38edbad6f..7d28b912c57 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -6,7 +6,7 @@ import { Identifier } from "../id/id" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" import { fn } from "@/util/fn" -import { Database, eq, desc } from "@/storage/db" +import { Database, eq, desc, inArray } from "@/storage/db" import { MessageTable, PartTable } from "./session.sql" import { ProviderTransform } from "@/provider/transform" import { STATUS_CODES } from "http" @@ -608,27 +608,56 @@ export namespace MessageV2 { } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { - const rows = Database.use((db) => - db - .select() - .from(MessageTable) - .where(eq(MessageTable.session_id, sessionID)) - .orderBy(desc(MessageTable.created_at)) - .all(), - ) - for (const row of rows) { - yield { - info: row.data, - parts: await parts(row.id), + const size = 50 + let offset = 0 + while (true) { + const rows = Database.use((db) => + db + .select() + .from(MessageTable) + .where(eq(MessageTable.session_id, sessionID)) + .orderBy(desc(MessageTable.created_at)) + .limit(size) + .offset(offset) + .all(), + ) + if (rows.length === 0) break + + const ids = rows.map((row) => row.id) + const partsByMessage = new Map() + if (ids.length > 0) { + const partRows = Database.use((db) => + db + .select() + .from(PartTable) + .where(inArray(PartTable.message_id, ids)) + .orderBy(PartTable.message_id, PartTable.id) + .all(), + ) + for (const row of partRows) { + const list = partsByMessage.get(row.message_id) + if (list) list.push(row.data) + else partsByMessage.set(row.message_id, [row.data]) + } + } + + for (const row of rows) { + yield { + info: row.data, + parts: partsByMessage.get(row.id) ?? [], + } } + + offset += rows.length + if (rows.length < size) break } }) export const parts = fn(Identifier.schema("message"), async (message_id) => { - const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).all()) - const result = rows.map((row) => row.data) - result.sort((a, b) => (a.id > b.id ? 1 : -1)) - return result + const rows = Database.use((db) => + db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), + ) + return rows.map((row) => row.data) }) export const get = fn( From a48a5a346277cb10fe6360693a080eda8ffa0508 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 12:36:05 -0500 Subject: [PATCH 10/17] 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 | 64 +- .../opencode/src/storage/json-migration.ts | 477 ++++++----- packages/opencode/src/storage/storage.ts | 227 +++++ .../test/storage/json-migration.test.ts | 56 +- 26 files changed, 1496 insertions(+), 1404 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 c3f0b7070d1..2d726247c14 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 d015c9aea98..25724edd9e8 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 4267ef64566..0686c705eea 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 95f019b1cd9..515ae153168 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 a68fd7f3e32..dcfc336d652 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 551a2384c5a..1b4fd556e9c 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 e25f0d3d56b..bc17ef49383 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 00000000000..63943f49d3b --- /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 efec141ea67..00000000000 --- 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 4ab81e184d6..00000000000 --- 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 c227eac1916..1d1532598d4 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 44e72950537..00000000000 --- 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 e73fda21b7b..a929f675d6f 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 efdcaba9909..a2be3733f85 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 443c47a534f..4085d99a373 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 d402310c3e4..ef9c7e2aace 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 7d52d87a4b0..2afaef5aa07 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 0183a9d3dff..53a52af331c 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 03bbcc148ee..6ef7cbaaf57 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 0cf978930e1..108db444c87 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 bf8a1904617..4d9c9290a55 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 f7bf4b3fa52..00000000000 --- 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 f49028a18b3..0beddca8f27 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 [] + 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 data = JSON.parse(readFileSync(file, "utf-8")) as { - entries: { tag: string; when: number }[] - } + 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 data.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) + return sql.sort((a, b) => a.timestamp - b.timestamp) } - const client = lazy(() => { + 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 4936387fd7c..4b235a9dc0e 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[], + } + + 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) + } + return items + } - // 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 + 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}`) } - db.insert(ProjectTable) - .values({ + 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}`) - } - } - 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 + 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}`) } - db.insert(SessionTable) - .values({ + } + log.info("migrated projects", { count: stats.projects }) + + 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 - } - // 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 - } - 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 }) + const sessionRows = db.select({ id: SessionTable.id }).from(SessionTable).all() + const sessionIds = new Set(sessionRows.map((item) => item.id)) - // 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 - } - db.insert(SessionDiffTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.diffs++ - } catch (e) { - stats.errors.push(`failed to migrate session_diff ${file}: ${e}`) + // 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 session diffs", { count: stats.diffs }) + log.info("migrated messages", { count: stats.messages }) + log.info("migrated parts", { count: stats.parts }) - // 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 + // 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, + }) + } + } + 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(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 todos", { count: stats.todos }) - // 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 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 }) + } + 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(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 }) + 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 + // 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 }) + } + 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, + }) - 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) }) + } - if (stats.errors.length > 0) { - log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) + return stats } - - return stats } diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts new file mode 100644 index 00000000000..18f2d67e7ac --- /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 70582eed045..bc55124002c 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() From f40685ab13610f57ca74c2cb005194e037680740 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 12:38:38 -0500 Subject: [PATCH 11/17] core: fix Drizzle ORM client initialization and type definitions --- packages/console/core/src/drizzle/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/core/src/drizzle/index.ts b/packages/console/core/src/drizzle/index.ts index f0f065de4a5..99e8612f875 100644 --- a/packages/console/core/src/drizzle/index.ts +++ b/packages/console/core/src/drizzle/index.ts @@ -14,7 +14,7 @@ export namespace Database { PlanetscaleQueryResultHKT, PlanetScalePreparedQueryHKT, Record, - ExtractTablesWithRelations> + ExtractTablesWithRelations, Record> > const client = memo(() => { @@ -23,7 +23,7 @@ export namespace Database { username: Resource.Database.username, password: Resource.Database.password, }) - const db = drizzle(result, {}) + const db = drizzle({ client: result }) return db }) From 63e38555c97e743cefc6c8f8a977a6bd5e40c1b4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 15:33:44 -0500 Subject: [PATCH 12/17] sync --- .../migration.sql | 18 ++++--- packages/opencode/src/project/project.sql.ts | 6 +-- packages/opencode/src/session/index.ts | 51 +++++++------------ packages/opencode/src/session/message-v2.ts | 22 +++++--- packages/opencode/src/session/session.sql.ts | 21 ++++---- packages/opencode/src/share/share.sql.ts | 2 + packages/opencode/src/storage/db.ts | 9 +++- .../opencode/src/storage/json-migration.ts | 16 +++--- .../test/storage/json-migration.test.ts | 4 +- 9 files changed, 83 insertions(+), 66 deletions(-) diff --git a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql index bc17ef49383..db59eb02131 100644 --- a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql +++ b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql @@ -14,7 +14,8 @@ CREATE TABLE `project` ( CREATE TABLE `message` ( `id` text PRIMARY KEY, `session_id` text NOT NULL, - `created_at` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); @@ -23,12 +24,16 @@ CREATE TABLE `part` ( `id` text PRIMARY KEY, `message_id` text NOT NULL, `session_id` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `permission` ( `project_id` text PRIMARY KEY, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); @@ -46,10 +51,7 @@ CREATE TABLE `session` ( `summary_deletions` integer, `summary_files` integer, `summary_diffs` text, - `revert_message_id` text, - `revert_part_id` text, - `revert_snapshot` text, - `revert_diff` text, + `revert` text, `permission` text, `time_created` integer NOT NULL, `time_updated` integer NOT NULL, @@ -65,6 +67,8 @@ CREATE TABLE `todo` ( `status` text NOT NULL, `priority` text NOT NULL, `position` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` 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 ); @@ -74,6 +78,8 @@ CREATE TABLE `session_share` ( `id` text NOT NULL, `secret` text NOT NULL, `url` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint @@ -82,4 +88,4 @@ CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoin 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 +CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 0f5a856e516..76a8ecdc3b9 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,4 +1,5 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { Database } from "@/storage/db" export const ProjectTable = sqliteTable("project", { id: text().primaryKey(), @@ -7,8 +8,7 @@ export const ProjectTable = sqliteTable("project", { name: text(), icon_url: text(), icon_color: text(), - time_created: integer().notNull(), - time_updated: integer().notNull(), + ...Database.Timestamps, time_initialized: integer(), sandboxes: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 4085d99a373..3a187354168 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -54,15 +54,7 @@ export namespace Session { } : undefined const share = row.share_url ? { url: row.share_url } : undefined - const revert = - row.revert_message_id !== null - ? { - messageID: row.revert_message_id, - partID: row.revert_part_id ?? undefined, - snapshot: row.revert_snapshot ?? undefined, - diff: row.revert_diff ?? undefined, - } - : undefined + const revert = row.revert ?? undefined return { id: row.id, slug: row.slug, @@ -98,10 +90,7 @@ export namespace Session { summary_deletions: info.summary?.deletions, summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, - revert_message_id: info.revert?.messageID ?? null, - revert_part_id: info.revert?.partID ?? null, - revert_snapshot: info.revert?.snapshot ?? null, - revert_diff: info.revert?.diff ?? null, + revert: info.revert ?? null, permission: info.permission, time_created: info.time.created, time_updated: info.time.updated, @@ -415,10 +404,7 @@ export namespace Session { const row = db .update(SessionTable) .set({ - revert_message_id: input.revert?.messageID ?? null, - revert_part_id: input.revert?.partID ?? null, - revert_snapshot: input.revert?.snapshot ?? null, - revert_diff: input.revert?.diff ?? null, + revert: input.revert ?? null, summary_additions: input.summary?.additions, summary_deletions: input.summary?.deletions, summary_files: input.summary?.files, @@ -440,10 +426,7 @@ export namespace Session { const row = db .update(SessionTable) .set({ - revert_message_id: null, - revert_part_id: null, - revert_snapshot: null, - revert_diff: null, + revert: null, time_updated: Date.now(), }) .where(eq(SessionTable.id, sessionID)) @@ -544,16 +527,17 @@ export namespace Session { }) export const updateMessage = fn(MessageV2.Info, async (msg) => { - const created_at = msg.role === "user" ? msg.time.created : msg.time.created + const time_created = msg.role === "user" ? msg.time.created : msg.time.created + const { id, sessionID, ...data } = msg Database.use((db) => { db.insert(MessageTable) .values({ - id: msg.id, - session_id: msg.sessionID, - created_at, - data: msg, + id, + session_id: sessionID, + time_created, + data, }) - .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data } }) .run() Database.effect(() => Bus.publish(MessageV2.Event.Updated, { @@ -620,15 +604,18 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (input) => { const part = "delta" in input ? input.part : input const delta = "delta" in input ? input.delta : undefined + const { id, messageID, sessionID, ...data } = part + const time = Date.now() Database.use((db) => { db.insert(PartTable) .values({ - id: part.id, - message_id: part.messageID, - session_id: part.sessionID, - data: part, + id, + message_id: messageID, + session_id: sessionID, + time_created: time, + data, }) - .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) + .onConflictDoUpdate({ target: PartTable.id, set: { data } }) .run() Database.effect(() => Bus.publish(MessageV2.Event.PartUpdated, { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 7d28b912c57..d5a5d176222 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -616,7 +616,7 @@ export namespace MessageV2 { .select() .from(MessageTable) .where(eq(MessageTable.session_id, sessionID)) - .orderBy(desc(MessageTable.created_at)) + .orderBy(desc(MessageTable.time_created)) .limit(size) .offset(offset) .all(), @@ -635,15 +635,22 @@ export namespace MessageV2 { .all(), ) for (const row of partRows) { + const part = { + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + } as MessageV2.Part const list = partsByMessage.get(row.message_id) - if (list) list.push(row.data) - else partsByMessage.set(row.message_id, [row.data]) + if (list) list.push(part) + else partsByMessage.set(row.message_id, [part]) } } for (const row of rows) { + const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info yield { - info: row.data, + info, parts: partsByMessage.get(row.id) ?? [], } } @@ -657,7 +664,9 @@ export namespace MessageV2 { const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), ) - return rows.map((row) => row.data) + return rows.map( + (row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part, + ) }) export const get = fn( @@ -668,8 +677,9 @@ export namespace MessageV2 { async (input) => { const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get()) if (!row) throw new Error(`Message not found: ${input.messageID}`) + const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info return { - info: row.data, + info, parts: await parts(input.messageID), } }, diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 2afaef5aa07..1eab1959225 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -3,6 +3,10 @@ import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "@/snapshot" import type { PermissionNext } from "@/permission/next" +import { Database } from "@/storage/db" + +type PartData = Omit +type InfoData = Omit export const SessionTable = sqliteTable( "session", @@ -21,13 +25,9 @@ export const SessionTable = sqliteTable( summary_deletions: integer(), summary_files: integer(), summary_diffs: text({ mode: "json" }).$type(), - revert_message_id: text(), - revert_part_id: text(), - revert_snapshot: text(), - revert_diff: text(), + revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), - time_created: integer().notNull(), - time_updated: integer().notNull(), + ...Database.Timestamps, time_compacting: integer(), time_archived: integer(), }, @@ -41,8 +41,8 @@ export const MessageTable = sqliteTable( session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - created_at: integer().notNull(), - data: text({ mode: "json" }).notNull().$type(), + ...Database.Timestamps, + data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("message_session_idx").on(table.session_id)], ) @@ -55,7 +55,8 @@ export const PartTable = sqliteTable( .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), session_id: text().notNull(), - data: text({ mode: "json" }).notNull().$type(), + ...Database.Timestamps, + data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], ) @@ -71,6 +72,7 @@ export const TodoTable = sqliteTable( status: text().notNull(), priority: text().notNull(), position: integer().notNull(), + ...Database.Timestamps, }, (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], ) @@ -79,5 +81,6 @@ export const PermissionTable = sqliteTable("permission", { project_id: text() .primaryKey() .references(() => ProjectTable.id, { onDelete: "cascade" }), + ...Database.Timestamps, data: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index 4d9c9290a55..1dcab7b8517 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,5 +1,6 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" +import { Database } from "@/storage/db" export const SessionShareTable = sqliteTable("session_share", { session_id: text() @@ -8,4 +9,5 @@ export const SessionShareTable = sqliteTable("session_share", { id: text().notNull(), secret: text().notNull(), url: text().notNull(), + ...Database.Timestamps, }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 0beddca8f27..370fa846c2f 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,7 +1,7 @@ import { Database as BunDatabase } from "bun:sqlite" import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { migrate } from "drizzle-orm/bun-sqlite/migrator" -import type { SQLiteTransaction } from "drizzle-orm/sqlite-core" +import { integer, type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { Context } from "../util/context" import { lazy } from "../util/lazy" @@ -137,4 +137,11 @@ export namespace Database { throw err } } + + export const Timestamps = { + time_created: integer().notNull(), + time_updated: integer() + .notNull() + .$onUpdate(() => Date.now()), + } } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 4b235a9dc0e..c1236a0f762 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -118,10 +118,7 @@ export namespace JsonMigration { summary_deletions: data.summary?.deletions ?? null, summary_files: data.summary?.files ?? null, summary_diffs: data.summary?.diffs ?? null, - revert_message_id: data.revert?.messageID ?? null, - revert_part_id: data.revert?.partID ?? null, - revert_snapshot: data.revert?.snapshot ?? null, - revert_diff: data.revert?.diff ?? null, + revert: data.revert ?? null, permission: data.permission ?? null, time_created: data.time?.created ?? Date.now(), time_updated: data.time?.updated ?? Date.now(), @@ -159,11 +156,13 @@ export namespace JsonMigration { stats.errors.push(`message missing id: ${item.file}`) continue } + const { id, sessionID: _, ...rest } = data values.push({ id: data.id, session_id: sessionID, - created_at: data.time?.created ?? Date.now(), - data, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + data: rest, }) messageIds.add(data.id) } @@ -191,11 +190,14 @@ export namespace JsonMigration { stats.errors.push(`part missing id or messageID: ${item.file}`) continue } + const { id, messageID, sessionID: _, ...rest } = data values.push({ id: data.id, message_id: data.messageID, session_id: sessionID, - data, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + data: rest, }) } if (values.length === 0) continue diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index bc55124002c..d72d4423587 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -196,11 +196,11 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const messages = db.select().from(MessageTable).all() expect(messages.length).toBe(1) - expect(messages[0].data.id).toBe("msg_test789ghi") + expect(messages[0].id).toBe("msg_test789ghi") const parts = db.select().from(PartTable).all() expect(parts.length).toBe(1) - expect(parts[0].data.id).toBe("prt_testabc123") + expect(parts[0].id).toBe("prt_testabc123") }) test("skips orphaned sessions (no parent project)", async () => { From 7706f5b6a8e0b8368a0eb2235ea43a9bb745f09f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 16:24:21 -0500 Subject: [PATCH 13/17] core: switch commit command to kimi-k2.5 and improve worktree test reliability --- .opencode/command/commit.md | 2 +- .../opencode/test/project/project.test.ts | 65 +++++++++++-------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 8260029195e..d8a420b1737 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,6 @@ --- description: git commit and push -model: opencode/glm-4.7 +model: opencode/kimi-k2.5 subtask: true --- diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index bee8b77dd18..36eea102314 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -54,37 +54,50 @@ describe("Project.fromDirectory with worktrees", () => { test("should set worktree to root when called from a worktree", async () => { await using tmp = await tmpdir({ git: true }) - const worktreePath = path.join(tmp.path, "..", "worktree-test") - await $`git worktree add ${worktreePath} -b test-branch`.cwd(tmp.path).quiet() - - const { project, sandbox } = await Project.fromDirectory(worktreePath) - - expect(project.worktree).toBe(tmp.path) - expect(sandbox).toBe(worktreePath) - expect(project.sandboxes).toContain(worktreePath) - expect(project.sandboxes).not.toContain(tmp.path) - - await $`git worktree remove ${worktreePath}`.cwd(tmp.path).quiet() + const worktreePath = path.join(tmp.path, "..", path.basename(tmp.path) + "-worktree") + try { + await $`git worktree add ${worktreePath} -b test-branch-${Date.now()}`.cwd(tmp.path).quiet() + + const { project, sandbox } = await Project.fromDirectory(worktreePath) + + expect(project.worktree).toBe(tmp.path) + expect(sandbox).toBe(worktreePath) + expect(project.sandboxes).toContain(worktreePath) + expect(project.sandboxes).not.toContain(tmp.path) + } finally { + await $`git worktree remove ${worktreePath}` + .cwd(tmp.path) + .quiet() + .catch(() => {}) + } }) test("should accumulate multiple worktrees in sandboxes", async () => { await using tmp = await tmpdir({ git: true }) - const worktree1 = path.join(tmp.path, "..", "worktree-1") - const worktree2 = path.join(tmp.path, "..", "worktree-2") - await $`git worktree add ${worktree1} -b branch-1`.cwd(tmp.path).quiet() - await $`git worktree add ${worktree2} -b branch-2`.cwd(tmp.path).quiet() - - await Project.fromDirectory(worktree1) - const { project } = await Project.fromDirectory(worktree2) - - expect(project.worktree).toBe(tmp.path) - expect(project.sandboxes).toContain(worktree1) - expect(project.sandboxes).toContain(worktree2) - expect(project.sandboxes).not.toContain(tmp.path) - - await $`git worktree remove ${worktree1}`.cwd(tmp.path).quiet() - await $`git worktree remove ${worktree2}`.cwd(tmp.path).quiet() + const worktree1 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt1") + const worktree2 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt2") + try { + await $`git worktree add ${worktree1} -b branch-${Date.now()}`.cwd(tmp.path).quiet() + await $`git worktree add ${worktree2} -b branch-${Date.now() + 1}`.cwd(tmp.path).quiet() + + await Project.fromDirectory(worktree1) + const { project } = await Project.fromDirectory(worktree2) + + expect(project.worktree).toBe(tmp.path) + expect(project.sandboxes).toContain(worktree1) + expect(project.sandboxes).toContain(worktree2) + expect(project.sandboxes).not.toContain(tmp.path) + } finally { + await $`git worktree remove ${worktree1}` + .cwd(tmp.path) + .quiet() + .catch(() => {}) + await $`git worktree remove ${worktree2}` + .cwd(tmp.path) + .quiet() + .catch(() => {}) + } }) }) From 0ccef1b31f8f3a285fc45dce5e54eee14cddb9be Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 16:41:24 -0500 Subject: [PATCH 14/17] sync --- packages/opencode/src/cli/cmd/import.ts | 2 +- packages/opencode/src/storage/db.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 5c0d9bdad57..d06cfe4d985 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -91,7 +91,7 @@ export const ImportCommand = cmd({ .values({ id: msg.info.id, session_id: exportData.info.id, - created_at: msg.info.time?.created ?? Date.now(), + time_created: msg.info.time?.created ?? Date.now(), data: msg.info, }) .onConflictDoNothing() diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 370fa846c2f..54a8acc34da 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -139,7 +139,9 @@ export namespace Database { } export const Timestamps = { - time_created: integer().notNull(), + time_created: integer() + .notNull() + .$default(() => Date.now()), time_updated: integer() .notNull() .$onUpdate(() => Date.now()), From cd174d8cbaefe8c0821400c197f133c5af38f277 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 16:57:51 -0500 Subject: [PATCH 15/17] sync --- packages/opencode/src/project/project.sql.ts | 4 +- packages/opencode/src/session/session.sql.ts | 12 +- packages/opencode/src/share/share.sql.ts | 4 +- packages/opencode/src/storage/db.ts | 11 +- packages/opencode/src/storage/schema.sql.ts | 10 ++ .../test/storage/json-migration.test.ts | 122 ++++++++++++++++++ 6 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 packages/opencode/src/storage/schema.sql.ts diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 76a8ecdc3b9..7c4dc94cc47 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" -import { Database } from "@/storage/db" +import { Timestamps } from "@/storage/schema.sql" export const ProjectTable = sqliteTable("project", { id: text().primaryKey(), @@ -8,7 +8,7 @@ export const ProjectTable = sqliteTable("project", { name: text(), icon_url: text(), icon_color: text(), - ...Database.Timestamps, + ...Timestamps, time_initialized: integer(), sandboxes: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 1eab1959225..ea7c1e32a37 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -3,7 +3,7 @@ import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "@/snapshot" import type { PermissionNext } from "@/permission/next" -import { Database } from "@/storage/db" +import { Timestamps } from "@/storage/schema.sql" type PartData = Omit type InfoData = Omit @@ -27,7 +27,7 @@ export const SessionTable = sqliteTable( summary_diffs: text({ mode: "json" }).$type(), revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), - ...Database.Timestamps, + ...Timestamps, time_compacting: integer(), time_archived: integer(), }, @@ -41,7 +41,7 @@ export const MessageTable = sqliteTable( session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - ...Database.Timestamps, + ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("message_session_idx").on(table.session_id)], @@ -55,7 +55,7 @@ export const PartTable = sqliteTable( .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), session_id: text().notNull(), - ...Database.Timestamps, + ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], @@ -72,7 +72,7 @@ export const TodoTable = sqliteTable( status: text().notNull(), priority: text().notNull(), position: integer().notNull(), - ...Database.Timestamps, + ...Timestamps, }, (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], ) @@ -81,6 +81,6 @@ export const PermissionTable = sqliteTable("permission", { project_id: text() .primaryKey() .references(() => ProjectTable.id, { onDelete: "cascade" }), - ...Database.Timestamps, + ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index 1dcab7b8517..268d41a6f61 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" -import { Database } from "@/storage/db" +import { Timestamps } from "@/storage/schema.sql" export const SessionShareTable = sqliteTable("session_share", { session_id: text() @@ -9,5 +9,5 @@ export const SessionShareTable = sqliteTable("session_share", { id: text().notNull(), secret: text().notNull(), url: text().notNull(), - ...Database.Timestamps, + ...Timestamps, }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 54a8acc34da..738e7c60259 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,7 +1,7 @@ import { Database as BunDatabase } from "bun:sqlite" import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { migrate } from "drizzle-orm/bun-sqlite/migrator" -import { integer, type SQLiteTransaction } from "drizzle-orm/sqlite-core" +import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { Context } from "../util/context" import { lazy } from "../util/lazy" @@ -137,13 +137,4 @@ export namespace Database { throw err } } - - export const Timestamps = { - time_created: integer() - .notNull() - .$default(() => Date.now()), - time_updated: integer() - .notNull() - .$onUpdate(() => Date.now()), - } } diff --git a/packages/opencode/src/storage/schema.sql.ts b/packages/opencode/src/storage/schema.sql.ts new file mode 100644 index 00000000000..ead3518dee0 --- /dev/null +++ b/packages/opencode/src/storage/schema.sql.ts @@ -0,0 +1,10 @@ +import { integer } from "drizzle-orm/sqlite-core" + +export const Timestamps = { + time_created: integer() + .notNull() + .$default(() => Date.now()), + time_updated: integer() + .notNull() + .$onUpdate(() => Date.now()), +} diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index d72d4423587..09578ebe8cf 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -11,6 +11,7 @@ import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" import { Project } from "../../src/project/project" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" +import { SessionShareTable } from "../../src/share/share.sql" // Test fixtures const fixtures = { @@ -240,4 +241,125 @@ describe("JSON to SQLite migration", () => { const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing }) + + test("migrates todos", async () => { + // First create the project and session + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + + // Create todo file (named by sessionID, contains array of todos) + await Bun.write( + path.join(storageDir, "todo", "ses_test456def.json"), + JSON.stringify([ + { + id: "todo_1", + content: "First todo", + status: "pending", + priority: "high", + }, + { + id: "todo_2", + content: "Second todo", + status: "completed", + priority: "medium", + }, + ]), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.todos).toBe(2) + + const db = drizzle({ client: sqlite }) + const todos = db.select().from(TodoTable).all() + expect(todos.length).toBe(2) + expect(todos[0].id).toBe("todo_1") + expect(todos[0].content).toBe("First todo") + expect(todos[0].status).toBe("pending") + expect(todos[0].priority).toBe("high") + expect(todos[0].position).toBe(0) + expect(todos[1].id).toBe("todo_2") + expect(todos[1].position).toBe(1) + }) + + test("migrates permissions", async () => { + // First create the project + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + + // Create permission file (named by projectID, contains array of rules) + const permissionData = [ + { permission: "file.read", pattern: "/test/file1.ts", action: "allow" as const }, + { permission: "file.write", pattern: "/test/file2.ts", action: "ask" as const }, + { permission: "command.run", pattern: "npm install", action: "deny" as const }, + ] + await Bun.write(path.join(storageDir, "permission", "proj_test123abc.json"), JSON.stringify(permissionData)) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.permissions).toBe(1) + + const db = drizzle({ client: sqlite }) + const permissions = db.select().from(PermissionTable).all() + expect(permissions.length).toBe(1) + expect(permissions[0].project_id).toBe("proj_test123abc") + expect(permissions[0].data).toEqual(permissionData) + }) + + test("migrates session shares", async () => { + // First create the project and session + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + + // Create session share file (named by sessionID) + await Bun.write( + path.join(storageDir, "session_share", "ses_test456def.json"), + JSON.stringify({ + id: "share_123", + secret: "supersecretkey", + url: "https://share.example.com/ses_test456def", + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.shares).toBe(1) + + const db = drizzle({ client: sqlite }) + const shares = db.select().from(SessionShareTable).all() + expect(shares.length).toBe(1) + expect(shares[0].session_id).toBe("ses_test456def") + expect(shares[0].id).toBe("share_123") + expect(shares[0].secret).toBe("supersecretkey") + expect(shares[0].url).toBe("https://share.example.com/ses_test456def") + }) }) From 19a41ab29778274a4f20ca8cf663d080abaf6ae1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 17:43:20 -0500 Subject: [PATCH 16/17] sync --- .../migration.sql | 5 +- .../snapshot.json | 130 ++++++++++++------ packages/opencode/src/session/session.sql.ts | 6 +- packages/opencode/src/session/todo.ts | 3 - .../opencode/src/storage/json-migration.ts | 5 +- .../test/storage/json-migration.test.ts | 43 +++++- 6 files changed, 139 insertions(+), 53 deletions(-) rename packages/opencode/migration/{20260127173238_melted_union_jack => 20260127222353_familiar_lady_ursula}/migration.sql (95%) rename packages/opencode/migration/{20260127173238_melted_union_jack => 20260127222353_familiar_lady_ursula}/snapshot.json (90%) diff --git a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql b/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql similarity index 95% rename from packages/opencode/migration/20260127173238_melted_union_jack/migration.sql rename to packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql index db59eb02131..775c1a1173d 100644 --- a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql +++ b/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql @@ -62,14 +62,13 @@ CREATE TABLE `session` ( --> statement-breakpoint CREATE TABLE `todo` ( `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, `time_created` integer NOT NULL, `time_updated` integer NOT NULL, - CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`), + CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`), CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint @@ -88,4 +87,4 @@ CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoin 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`); +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/20260127222353_familiar_lady_ursula/snapshot.json similarity index 90% rename from packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json rename to packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json index 63943f49d3b..1e297d3f8a9 100644 --- a/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json +++ b/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json @@ -1,7 +1,7 @@ { "version": "7", "dialect": "sqlite", - "id": "0e365b40-39c4-447f-9729-9714d865d8ff", + "id": "068758ed-a97a-46f6-8a59-6c639ae7c20c", "prevIds": [ "00000000-0000-0000-0000-000000000000" ], @@ -160,7 +160,17 @@ "autoincrement": false, "default": null, "generated": null, - "name": "created_at", + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", "entityType": "columns", "table": "message" }, @@ -204,6 +214,26 @@ "entityType": "columns", "table": "part" }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, { "type": "text", "notNull": true, @@ -224,6 +254,26 @@ "entityType": "columns", "table": "permission" }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, { "type": "text", "notNull": true, @@ -360,37 +410,7 @@ "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", + "name": "revert", "entityType": "columns", "table": "session" }, @@ -460,7 +480,7 @@ "autoincrement": false, "default": null, "generated": null, - "name": "id", + "name": "content", "entityType": "columns", "table": "todo" }, @@ -470,7 +490,7 @@ "autoincrement": false, "default": null, "generated": null, - "name": "content", + "name": "status", "entityType": "columns", "table": "todo" }, @@ -480,17 +500,17 @@ "autoincrement": false, "default": null, "generated": null, - "name": "status", + "name": "priority", "entityType": "columns", "table": "todo" }, { - "type": "text", + "type": "integer", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "priority", + "name": "position", "entityType": "columns", "table": "todo" }, @@ -500,7 +520,17 @@ "autoincrement": false, "default": null, "generated": null, - "name": "position", + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", "entityType": "columns", "table": "todo" }, @@ -544,6 +574,26 @@ "entityType": "columns", "table": "session_share" }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, { "columns": [ "session_id" @@ -637,7 +687,7 @@ { "columns": [ "session_id", - "id" + "position" ], "nameExplicit": false, "name": "todo_pk", diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index ea7c1e32a37..9c5c72c4c57 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -67,14 +67,16 @@ export const TodoTable = sqliteTable( session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - id: text().notNull(), content: text().notNull(), status: text().notNull(), priority: text().notNull(), position: integer().notNull(), ...Timestamps, }, - (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], + (table) => [ + primaryKey({ columns: [table.session_id, table.position] }), + index("todo_session_idx").on(table.session_id), + ], ) export const PermissionTable = sqliteTable("permission", { diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 6ef7cbaaf57..ec2bcdda3c4 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -10,7 +10,6 @@ export namespace Todo { content: z.string().describe("Brief description of the task"), status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"), priority: z.string().describe("Priority level of the task: high, medium, low"), - id: z.string().describe("Unique identifier for the todo item"), }) .meta({ ref: "Todo" }) export type Info = z.infer @@ -33,7 +32,6 @@ export namespace Todo { .values( input.todos.map((todo, position) => ({ session_id: input.sessionID, - id: todo.id, content: todo.content, status: todo.status, priority: todo.priority, @@ -50,7 +48,6 @@ export namespace Todo { 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/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index c1236a0f762..0debe2e2e90 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -235,14 +235,15 @@ export namespace JsonMigration { } for (let position = 0; position < data.length; position++) { const todo = data[position] - if (!todo?.id || !todo?.content || !todo?.status || !todo?.priority) continue + if (!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, + time_created: Date.now(), + time_updated: Date.now(), }) } } diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 09578ebe8cf..f9be0d80410 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -282,17 +282,54 @@ describe("JSON to SQLite migration", () => { expect(stats?.todos).toBe(2) const db = drizzle({ client: sqlite }) - const todos = db.select().from(TodoTable).all() + const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all() expect(todos.length).toBe(2) - expect(todos[0].id).toBe("todo_1") expect(todos[0].content).toBe("First todo") expect(todos[0].status).toBe("pending") expect(todos[0].priority).toBe("high") expect(todos[0].position).toBe(0) - expect(todos[1].id).toBe("todo_2") + expect(todos[1].content).toBe("Second todo") expect(todos[1].position).toBe(1) }) + test("todos are ordered by position", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + + await Bun.write( + path.join(storageDir, "todo", "ses_test456def.json"), + JSON.stringify([ + { content: "Third", status: "pending", priority: "low" }, + { content: "First", status: "pending", priority: "high" }, + { content: "Second", status: "in_progress", priority: "medium" }, + ]), + ) + + await JsonMigration.run(sqlite) + + const db = drizzle({ client: sqlite }) + const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all() + + expect(todos.length).toBe(3) + expect(todos[0].content).toBe("Third") + expect(todos[0].position).toBe(0) + expect(todos[1].content).toBe("First") + expect(todos[1].position).toBe(1) + expect(todos[2].content).toBe("Second") + expect(todos[2].position).toBe(2) + }) + test("migrates permissions", async () => { // First create the project await Bun.write( From e0ffbe2ff50e3206c1cfbc18182c0df53eb14194 Mon Sep 17 00:00:00 2001 From: Github Action Date: Tue, 27 Jan 2026 22:46:45 +0000 Subject: [PATCH 17/17] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 0b735b35d6e..9d2bb999d6e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-9oI1gekRbjY6L8VwlkLdPty/9rCxC20EJlESkazEX8Y=", - "aarch64-linux": "sha256-vn+eCVanOSNfjyqHRJn4VdqbpdMoBFm49REuIkByAio=", - "aarch64-darwin": "sha256-0dMP5WbqDq3qdLRrKfmCjXz2kUDjTttGTqD3v6PDbkg=", - "x86_64-darwin": "sha256-9dEWluRXY7RTPdSEhhPsDJeGo+qa3V8dqh6n6WsLeGw=" + "x86_64-linux": "sha256-L3peTVUra1QZIv1We+yuXnia1Eq3Je206BLlqVOswL8=", + "aarch64-linux": "sha256-d0vqy64cUpqrkLTPEC+6JrGhD5msfivrXBwpLjs/qD4=", + "aarch64-darwin": "sha256-iqxztCONsuV77SGEJCCenwew9QWCx9NPzY6PlO+872I=", + "x86_64-darwin": "sha256-FwrCnbJ5kGxewoXK+lMk0I0UtYvGcOZjOb2Xhzmqwkk=" } }