From 1e7b4768b13d90043fc29377fdfa573b3c0373d0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 24 Jan 2026 11:50:25 -0500 Subject: [PATCH 01/35] 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 34a6488ba0..6211ff1482 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 0000000000..551a2384c5 --- /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 0000000000..e25f0d3d56 --- /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 0000000000..efec141ea6 --- /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 0000000000..4ab81e184d --- /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 822b581dd7..6c3f3a5cca 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 0000000000..f5eaf79323 --- /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 0000000000..47c2e0c5e1 --- /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 0000000000..5b3c1485f3 --- /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 9d7e8c5617..c78776b9d7 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 d78c4f0abd..21ee97fa82 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 6dc5e99e91..e73fda21b7 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 2481f104ed..b625bb57fc 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 0000000000..651d537cf2 --- /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 f6902de4e1..71c6a9bc7b 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 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 list() { + return 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() + 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 26e2dfcb12..cc5fa96187 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 1ac6cf7971..d1f3820cb8 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 fa646f21ea..0ffb10e0b6 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 b81a21a57b..84b567b942 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 83ca72addb..2dab09918a 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 7afe44e2ce..fb6e0a5ec3 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 0000000000..be35dd1703 --- /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 611d5f1c62..a79850046d 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 aa7df7e981..3280744662 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 dddce95cb4..2d16820459 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 0000000000..7a65fd764b --- /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 0000000000..28cb1e2649 --- /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 0000000000..3c1a159305 --- /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 0000000000..96f3714c2a --- /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 0000000000..a048c61efb --- /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 18f2d67e7a..0000000000 --- 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 0cc6d8d5c4..55643dc6a7 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 97fe2c4fc0..30443d36b1 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 29f1efa401..add3332048 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 819166c94c..f40e4b1e9a 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 d44e606746..bee8b77dd1 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 0000000000..c1038ab311 --- /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/35] 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 6c3f3a5cca..a3b00771d3 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 5b3c1485f3..949f128ba6 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 c78776b9d7..7f97b70c31 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 21ee97fa82..1f7263b325 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 b625bb57fc..98840867b2 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 71c6a9bc7b..fc940b9588 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 84b567b942..db06483eea 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 2dab09918a..e92252400c 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 fb6e0a5ec3..6a27fc7b9a 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 a79850046d..2472eee7a3 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 3280744662..8ba5a0281c 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 2d16820459..0f18cb974d 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 3c1a159305..475da9475b 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 30443d36b1..0afffda999 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/35] 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 d5559fdfb1..af22d7711a 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/35] 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 | 103 ++++++++------- 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 | 103 +++++++-------- .../opencode/src/storage/json-migration.ts | 17 +-- .../src/storage/migrations.generated.ts | 6 - .../test/storage/json-migration.test.ts | 27 ++-- 10 files changed, 259 insertions(+), 189 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 c157fa32d3..6119e653e1 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`. - -Good: +Prefer single word variable names. Only use multiple words if necessary. ```ts -const foo = condition ? 1 : 2 +// Good +const foo = 1 +const bar = 2 + +// Bad +const fooBar = 1 +const barBaz = 2 ``` -Bad: +Reduce total variable count by inlining when a value is only used once. ```ts -let foo +// 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() +``` + +### Destructuring + +Avoid unnecessary destructuring. Use dot notation to preserve context. + +```ts +// 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 +### Control Flow -Prefer early returns or using an `iife` to avoid else statements. - -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 f65985e9b5..d015c9aea9 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 3fc8b3c442..c227eac191 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 12902b1cfc..06ed45e4d2 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 47c2e0c5e1..0000000000 --- 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 949f128ba6..1fda74cf6b 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 475da9475b..824bc85658 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) + const db = drizzle({ client: sqlite }) + migrate(db) - migrateFromJson(sqlite).catch((e) => log.error("json migration failed", { error: e })) + // 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 - ) - `) +type MigrationsJournal = { sql: string; timestamp: number }[] - const applied = new Set( - sqlite - .query<{ name: string }, []>("SELECT name FROM _migrations") - .all() - .map((r) => r.name), - ) +function prepareJournal(dir: string): MigrationsJournal { + const file = path.join(dir, "meta/_journal.json") + if (!Bun.file(file).size) return [] - 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 - } - } - - 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 96f3714c2a..df15ff6df0 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 a048c61efb..0000000000 --- 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 c1038ab311..9be299bac3 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/35] 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 | 211 +++++++++++++----- packages/opencode/src/session/message-v2.ts | 8 +- packages/opencode/src/session/prompt.ts | 29 +-- 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 6119e653e1..750aeff1de 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 1fda74cf6b..44e7295053 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 7f97b70c31..5c0d9bdad5 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 98840867b2..1e1df62a3c 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 651d537cf2..0f5a856e51 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 fc940b9588..715b4f3230 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 3850376bdb..de1ebffe70 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 db06483eea..5ad143c93e 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 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 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 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 e92252400c..6e38edbad6 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 8554b44a72..fc10d7c780 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 + 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 - draft.title = title - }, - { touch: false }, - ) + 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 6a27fc7b9a..a4356dc70a 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 be35dd1703..7d52d87a4b 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 2472eee7a3..0183a9d3df 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 8ba5a0281c..03bbcc148e 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 0f18cb974d..0cf978930e 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 7a65fd764b..bf8a190461 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 824bc85658..1f6cc30807 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 df15ff6df0..4936387fd7 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 9be299bac3..b9981858ba 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/35] 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 c73a520428..a3ea493cc2 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 b9981858ba..70582eed04 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/35] core: remove dependency on remeda to simplify dependencies --- packages/opencode/src/project/project.ts | 187 ++++++++++++----------- packages/opencode/src/session/revert.ts | 49 ++++-- 2 files changed, 128 insertions(+), 108 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 715b4f3230..964ff11c02 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,63 +74,60 @@ 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) - - if (!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) + 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 (!cached && !roots) { + return { + id: "global", + worktree: sandbox, + sandbox: sandbox, + vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), } } + const id = cached ?? roots?.[0] + if (!cached && id) { + void Bun.file(path.join(git, "opencode")) + .write(id) + .catch(() => undefined) + } + if (!id) { return { id: "global", @@ -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 a4356dc70a..d402310c3e 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/35] 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 5ad143c93e..443c47a534 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 1f6cc30807..f49028a18b 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/35] 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 6e38edbad6..7d28b912c5 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/35] core: migrate from custom JSON storage to standard Drizzle migrations to improve database reliability and performance This replaces the previous manual JSON file system with standard Drizzle migrations, enabling: - Proper database schema migrations with timestamp-based versioning - Batched migration for faster migration of large datasets - Better data integrity with proper table schemas instead of JSON blobs - Easier database upgrades and rollback capabilities Migration changes: - Todo table now uses individual columns with composite PK instead of JSON blob - Share table removes unused download share data - Session diff table moved from database table to file storage - All migrations now use proper Drizzle format with per-folder layout Users will see a one-time migration on next run that migrates existing JSON data to the new SQLite database. --- .opencode/opencode.jsonc | 6 - bun.lock | 177 +--- package.json | 2 + packages/console/core/package.json | 4 +- packages/opencode/AGENTS.md | 33 +- packages/opencode/drizzle.config.ts | 3 + .../migration.sql} | 58 +- .../snapshot.json | 787 ++++++++++++++++++ .../migration/meta/0000_snapshot.json | 587 ------------- .../opencode/migration/meta/_journal.json | 13 - packages/opencode/package.json | 6 +- packages/opencode/src/cli/cmd/database.ts | 144 ---- packages/opencode/src/index.ts | 13 +- packages/opencode/src/project/bootstrap.ts | 2 - packages/opencode/src/session/index.ts | 28 +- packages/opencode/src/session/revert.ts | 11 +- packages/opencode/src/session/session.sql.ts | 30 +- packages/opencode/src/session/summary.ts | 20 +- packages/opencode/src/session/todo.ts | 36 +- packages/opencode/src/share/share-next.ts | 10 +- packages/opencode/src/share/share.sql.ts | 14 +- packages/opencode/src/share/share.ts | 92 -- packages/opencode/src/storage/db.ts | 68 +- .../opencode/src/storage/json-migration.ts | 479 ++++++----- packages/opencode/src/storage/storage.ts | 227 +++++ .../test/storage/json-migration.test.ts | 56 +- 26 files changed, 1499 insertions(+), 1407 deletions(-) rename packages/opencode/migration/{0000_magical_strong_guy.sql => 20260127173238_melted_union_jack/migration.sql} (58%) create mode 100644 packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json delete mode 100644 packages/opencode/migration/meta/0000_snapshot.json delete mode 100644 packages/opencode/migration/meta/_journal.json delete mode 100644 packages/opencode/src/cli/cmd/database.ts delete mode 100644 packages/opencode/src/share/share.ts create mode 100644 packages/opencode/src/storage/storage.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index c3f0b7070d..2d726247c1 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -9,12 +9,6 @@ "options": {}, }, }, - "mcp": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/mcp", - }, - }, "tools": { "github-triage": false, "github-pr-search": false, diff --git a/bun.lock b/bun.lock index d015c9aea9..25724edd9e 100644 --- a/bun.lock +++ b/bun.lock @@ -115,7 +115,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -127,7 +127,7 @@ "@types/bun": "1.3.0", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", }, @@ -354,6 +354,7 @@ "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -524,6 +525,8 @@ "ai": "5.0.119", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -849,7 +852,7 @@ "@dot/log": ["@dot/log@0.1.5", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -861,10 +864,6 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -2353,9 +2352,9 @@ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2417,8 +2416,6 @@ "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -4127,8 +4124,6 @@ "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], "@gitlab/gitlab-ai-provider/openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="], @@ -4381,9 +4376,11 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "db0/drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], @@ -4469,10 +4466,6 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "opencode/drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - - "opencode/drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], - "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], @@ -4645,50 +4638,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], @@ -5017,51 +4966,55 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5085,10 +5038,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "opencode/drizzle-kit/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - - "opencode/drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], @@ -5315,56 +5264,6 @@ "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opencode/drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "opencode/drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "opencode/drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "opencode/drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], diff --git a/package.json b/package.json index 4267ef6456..0686c705ee 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "ai": "5.0.119", "hono": "4.10.7", "hono-openapi": "1.1.2", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 95f019b1cd..515ae15316 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -12,7 +12,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -43,7 +43,7 @@ "@tsconfig/node22": "22.0.2", "@types/bun": "1.3.0", "@types/node": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", "@typescript/native-preview": "catalog:" diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index a68fd7f3e3..dcfc336d65 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -1,27 +1,10 @@ -# opencode agent guidelines +# opencode database guide -## Build/Test Commands +## Database -- **Install**: `bun install` -- **Run**: `bun run --conditions=browser ./src/index.ts` -- **Typecheck**: `bun run typecheck` (npm run typecheck) -- **Test**: `bun test` (runs all tests) -- **Single test**: `bun test test/tool/tool.test.ts` (specific test file) - -## Code Style - -- **Runtime**: Bun with TypeScript ESM modules -- **Imports**: Use relative imports for local modules, named imports preferred -- **Types**: Zod schemas for validation, TypeScript interfaces for structure -- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces -- **Error handling**: Use Result patterns, avoid throwing exceptions in tools -- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`) - -## Architecture - -- **Tools**: Implement `Tool.Info` interface with `execute()` method -- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI -- **Validation**: All inputs validated with Zod schemas -- **Logging**: Use `Log.create({ service: "name" })` pattern -- **Storage**: Use `Storage` namespace for persistence -- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files. +- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`. +- **Naming**: tables and columns use snake*case; join columns are `_id`; indexes are `*\_idx`. +- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`). +- **Command**: `bun run db generate --name `. +- **Output**: creates `migration/_/migration.sql` and `snapshot.json`. +- **Tests**: migration tests should read the per-folder layout (no `_journal.json`). diff --git a/packages/opencode/drizzle.config.ts b/packages/opencode/drizzle.config.ts index 551a2384c5..1b4fd556e9 100644 --- a/packages/opencode/drizzle.config.ts +++ b/packages/opencode/drizzle.config.ts @@ -4,4 +4,7 @@ export default defineConfig({ dialect: "sqlite", schema: "./src/**/*.sql.ts", out: "./migration", + dbCredentials: { + url: "/home/thdxr/.local/share/opencode/opencode.db", + }, }) diff --git a/packages/opencode/migration/0000_magical_strong_guy.sql b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql similarity index 58% rename from packages/opencode/migration/0000_magical_strong_guy.sql rename to packages/opencode/migration/20260127173238_melted_union_jack/migration.sql index e25f0d3d56..bc17ef4938 100644 --- a/packages/opencode/migration/0000_magical_strong_guy.sql +++ b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql @@ -1,5 +1,5 @@ CREATE TABLE `project` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `worktree` text NOT NULL, `vcs` text, `name` text, @@ -12,38 +12,29 @@ CREATE TABLE `project` ( ); --> statement-breakpoint CREATE TABLE `message` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `session_id` text NOT NULL, `created_at` integer NOT NULL, `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint CREATE TABLE `part` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `message_id` text NOT NULL, `session_id` text NOT NULL, `data` text NOT NULL, - FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint -CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint CREATE TABLE `permission` ( - `project_id` text PRIMARY KEY NOT NULL, + `project_id` text PRIMARY KEY, `data` text NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `session_diff` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, + `id` text PRIMARY KEY, `project_id` text NOT NULL, `parent_id` text, `slug` text NOT NULL, @@ -64,24 +55,31 @@ CREATE TABLE `session` ( `time_updated` integer NOT NULL, `time_compacting` integer, `time_archived` integer, - FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade + CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint -CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint CREATE TABLE `todo` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + `session_id` text NOT NULL, + `id` text NOT NULL, + `content` text NOT NULL, + `status` text NOT NULL, + `priority` text NOT NULL, + `position` integer NOT NULL, + CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`), + CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `session_share` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade + `session_id` text PRIMARY KEY, + `id` text NOT NULL, + `secret` text NOT NULL, + `url` text NOT NULL, + CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint -CREATE TABLE `share` ( - `session_id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL -); +CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint +CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint +CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint +CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint +CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint +CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); \ No newline at end of file diff --git a/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json b/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json new file mode 100644 index 0000000000..63943f49d3 --- /dev/null +++ b/packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json @@ -0,0 +1,787 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "0e365b40-39c4-447f-9729-9714d865d8ff", + "prevIds": [ + "00000000-0000-0000-0000-000000000000" + ], + "ddl": [ + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_message_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_part_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_snapshot", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert_diff", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "session_id", + "id" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/migration/meta/0000_snapshot.json b/packages/opencode/migration/meta/0000_snapshot.json deleted file mode 100644 index efec141ea6..0000000000 --- a/packages/opencode/migration/meta/0000_snapshot.json +++ /dev/null @@ -1,587 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "797eb060-2c45-4abf-925d-6b8375dd8a64", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "project": { - "name": "project", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "worktree": { - "name": "worktree", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "vcs": { - "name": "vcs", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon_url": { - "name": "icon_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon_color": { - "name": "icon_color", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_created": { - "name": "time_created", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_updated": { - "name": "time_updated", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_initialized": { - "name": "time_initialized", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sandboxes": { - "name": "sandboxes", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "message": { - "name": "message", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "message_session_idx": { - "name": "message_session_idx", - "columns": [ - "session_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "message_session_id_session_id_fk": { - "name": "message_session_id_session_id_fk", - "tableFrom": "message", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "part": { - "name": "part", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "message_id": { - "name": "message_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "part_message_idx": { - "name": "part_message_idx", - "columns": [ - "message_id" - ], - "isUnique": false - }, - "part_session_idx": { - "name": "part_session_idx", - "columns": [ - "session_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "part_message_id_message_id_fk": { - "name": "part_message_id_message_id_fk", - "tableFrom": "part", - "tableTo": "message", - "columnsFrom": [ - "message_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "permission": { - "name": "permission", - "columns": { - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "permission_project_id_project_id_fk": { - "name": "permission_project_id_project_id_fk", - "tableFrom": "permission", - "tableTo": "project", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session_diff": { - "name": "session_diff", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_diff_session_id_session_id_fk": { - "name": "session_diff_session_id_session_id_fk", - "tableFrom": "session_diff", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session": { - "name": "session", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "parent_id": { - "name": "parent_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "directory": { - "name": "directory", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "share_url": { - "name": "share_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_additions": { - "name": "summary_additions", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_deletions": { - "name": "summary_deletions", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_files": { - "name": "summary_files", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "summary_diffs": { - "name": "summary_diffs", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_message_id": { - "name": "revert_message_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_part_id": { - "name": "revert_part_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_snapshot": { - "name": "revert_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "revert_diff": { - "name": "revert_diff", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "permission": { - "name": "permission", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_created": { - "name": "time_created", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_updated": { - "name": "time_updated", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "time_compacting": { - "name": "time_compacting", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "time_archived": { - "name": "time_archived", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "session_project_idx": { - "name": "session_project_idx", - "columns": [ - "project_id" - ], - "isUnique": false - }, - "session_parent_idx": { - "name": "session_parent_idx", - "columns": [ - "parent_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "session_project_id_project_id_fk": { - "name": "session_project_id_project_id_fk", - "tableFrom": "session", - "tableTo": "project", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "todo": { - "name": "todo", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "todo_session_id_session_id_fk": { - "name": "todo_session_id_session_id_fk", - "tableFrom": "todo", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "session_share": { - "name": "session_share", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_share_session_id_session_id_fk": { - "name": "session_share_session_id_session_id_fk", - "tableFrom": "session_share", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "share": { - "name": "share", - "columns": { - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/packages/opencode/migration/meta/_journal.json b/packages/opencode/migration/meta/_journal.json deleted file mode 100644 index 4ab81e184d..0000000000 --- a/packages/opencode/migration/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1769232577135, - "tag": "0000_magical_strong_guy", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c227eac191..1d1532598d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -26,7 +26,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "drizzle-kit": "1.0.0-beta.12-a5629fb", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", @@ -43,6 +42,8 @@ "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -122,5 +123,8 @@ "yargs": "18.0.0", "zod": "catalog:", "zod-to-json-schema": "3.24.5" + }, + "overrides": { + "drizzle-orm": "1.0.0-beta.12-a5629fb" } } diff --git a/packages/opencode/src/cli/cmd/database.ts b/packages/opencode/src/cli/cmd/database.ts deleted file mode 100644 index 44e7295053..0000000000 --- a/packages/opencode/src/cli/cmd/database.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { Argv } from "yargs" -import { cmd } from "./cmd" -import { bootstrap } from "../bootstrap" -import { UI } from "../ui" -import { Database } from "../../storage/db" -import { ProjectTable } from "../../project/project.sql" -import { Project } from "../../project/project" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../../session/session.sql" -import { Session } from "../../session" -import { SessionShareTable, ShareTable } from "../../share/share.sql" -import path from "path" -import fs from "fs/promises" - -export const DatabaseCommand = cmd({ - command: "database", - describe: "database management commands", - builder: (yargs) => yargs.command(ExportCommand).demandCommand(), - async handler() {}, -}) - -const ExportCommand = cmd({ - command: "export", - describe: "export database to JSON files", - builder: (yargs: Argv) => { - return yargs.option("output", { - alias: ["o"], - describe: "output directory", - type: "string", - demandOption: true, - }) - }, - handler: async (args) => { - await bootstrap(process.cwd(), async () => { - const outDir = path.resolve(args.output) - await fs.mkdir(outDir, { recursive: true }) - - const stats = { - projects: 0, - sessions: 0, - messages: 0, - parts: 0, - diffs: 0, - todos: 0, - permissions: 0, - sessionShares: 0, - shares: 0, - } - - // Export projects - const projectDir = path.join(outDir, "project") - await fs.mkdir(projectDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(ProjectTable).all())) { - const project = Project.fromRow(row) - await Bun.write(path.join(projectDir, `${row.id}.json`), JSON.stringify(project, null, 2)) - stats.projects++ - } - - // Export sessions (organized by projectID) - const sessionDir = path.join(outDir, "session") - for (const row of Database.use((db) => db.select().from(SessionTable).all())) { - const dir = path.join(sessionDir, row.project_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(Session.fromRow(row), null, 2)) - stats.sessions++ - } - - // Export messages (organized by sessionID) - const messageDir = path.join(outDir, "message") - for (const row of Database.use((db) => db.select().from(MessageTable).all())) { - const dir = path.join(messageDir, row.session_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) - stats.messages++ - } - - // Export parts (organized by messageID) - const partDir = path.join(outDir, "part") - for (const row of Database.use((db) => db.select().from(PartTable).all())) { - const dir = path.join(partDir, row.message_id) - await fs.mkdir(dir, { recursive: true }) - await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2)) - stats.parts++ - } - - // Export session diffs - const diffDir = path.join(outDir, "session_diff") - await fs.mkdir(diffDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(SessionDiffTable).all())) { - await Bun.write(path.join(diffDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.diffs++ - } - - // Export todos - const todoDir = path.join(outDir, "todo") - await fs.mkdir(todoDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(TodoTable).all())) { - await Bun.write(path.join(todoDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.todos++ - } - - // Export permissions - const permDir = path.join(outDir, "permission") - await fs.mkdir(permDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(PermissionTable).all())) { - await Bun.write(path.join(permDir, `${row.project_id}.json`), JSON.stringify(row.data, null, 2)) - stats.permissions++ - } - - // Export session shares - const sessionShareDir = path.join(outDir, "session_share") - await fs.mkdir(sessionShareDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(SessionShareTable).all())) { - await Bun.write(path.join(sessionShareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.sessionShares++ - } - - // Export shares - const shareDir = path.join(outDir, "share") - await fs.mkdir(shareDir, { recursive: true }) - for (const row of Database.use((db) => db.select().from(ShareTable).all())) { - await Bun.write(path.join(shareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2)) - stats.shares++ - } - - UI.println(`Exported to ${outDir}:`) - UI.println(` ${stats.projects} projects`) - UI.println(` ${stats.sessions} sessions`) - UI.println(` ${stats.messages} messages`) - UI.println(` ${stats.parts} parts`) - UI.println(` ${stats.diffs} session diffs`) - UI.println(` ${stats.todos} todos`) - UI.println(` ${stats.permissions} permissions`) - UI.println(` ${stats.sessionShares} session shares`) - UI.println(` ${stats.shares} shares`) - }) - }, -}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index e73fda21b7..a929f675d6 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,7 +26,10 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" -import { DatabaseCommand } from "./cli/cmd/database" +import path from "path" +import { Global } from "./global" +import { JsonMigration } from "./storage/json-migration" +import { Database } from "./storage/db" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -75,6 +78,13 @@ const cli = yargs(hideBin(process.argv)) version: Installation.VERSION, args: process.argv.slice(2), }) + + const marker = path.join(Global.Path.data, "opencode.db") + if (!(await Bun.file(marker).exists())) { + console.log("Performing one time database migration, may take a few minutes...") + await JsonMigration.run(Database.Client().$client) + console.log("Database migration complete.") + } }) .usage("\n" + UI.logo()) .completion("completion", "generate shell completion script") @@ -98,7 +108,6 @@ const cli = yargs(hideBin(process.argv)) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) - .command(DatabaseCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index efdcaba990..a2be3733f8 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -1,5 +1,4 @@ import { Plugin } from "../plugin" -import { Share } from "../share/share" import { Format } from "../format" import { LSP } from "../lsp" import { FileWatcher } from "../file/watcher" @@ -17,7 +16,6 @@ import { Truncate } from "../tool/truncation" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) await Plugin.init() - Share.init() ShareNext.init() Format.init() await LSP.init() diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 443c47a534..4085d99a37 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -11,8 +11,8 @@ import { Identifier } from "../id/id" import { Installation } from "../installation" import { Database, NotFoundError, eq } from "../storage/db" -import { SessionTable, MessageTable, PartTable, SessionDiffTable } from "./session.sql" -import { ShareTable } from "../share/share.sql" +import { SessionTable, MessageTable, PartTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" @@ -153,16 +153,6 @@ export namespace Session { }) export type Info = z.output - export const ShareInfo = z - .object({ - secret: z.string(), - url: z.string(), - }) - .meta({ - ref: "SessionShare", - }) - export type ShareInfo = z.output - export const Event = { Created: BusEvent.define( "session.created", @@ -323,11 +313,6 @@ export namespace Session { return fromRow(row) }) - export const getShare = fn(Identifier.schema("session"), async (id) => { - const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.session_id, id)).get()) - return row?.data - }) - export const share = fn(Identifier.schema("session"), async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { @@ -498,10 +483,11 @@ export namespace Session { ) export const diff = fn(Identifier.schema("session"), async (sessionID) => { - const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, sessionID)).get(), - ) - return row?.data ?? [] + try { + return await Storage.read(["session_diff", sessionID]) + } catch { + return [] + } }) export const messages = fn( diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index d402310c3e..ef9c7e2aac 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -5,7 +5,8 @@ import { MessageV2 } from "./message-v2" import { Session } from "." import { Log } from "../util/log" import { Database, eq } from "../storage/db" -import { SessionDiffTable, MessageTable, PartTable } from "./session.sql" +import { MessageTable, PartTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Bus } from "../bus" import { SessionPrompt } from "./prompt" import { SessionSummary } from "./summary" @@ -60,13 +61,7 @@ export namespace SessionRevert { if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot) const rangeMessages = all.filter((msg) => msg.info.id >= revert!.messageID) const diffs = await SessionSummary.computeDiff({ messages: rangeMessages }) - Database.use((db) => - db - .insert(SessionDiffTable) - .values({ session_id: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) - .run(), - ) + await Storage.write(["session_diff", input.sessionID], diffs) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 7d52d87a4b..2afaef5aa0 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -1,8 +1,7 @@ -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "@/snapshot" -import type { Todo } from "./todo" import type { PermissionNext } from "@/permission/next" export const SessionTable = sqliteTable( @@ -61,19 +60,20 @@ export const PartTable = sqliteTable( (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], ) -export const SessionDiffTable = sqliteTable("session_diff", { - session_id: text() - .primaryKey() - .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type(), -}) - -export const TodoTable = sqliteTable("todo", { - session_id: text() - .primaryKey() - .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type(), -}) +export const TodoTable = sqliteTable( + "todo", + { + session_id: text() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + id: text().notNull(), + content: text().notNull(), + status: text().notNull(), + priority: text().notNull(), + position: integer().notNull(), + }, + (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], +) export const PermissionTable = sqliteTable("permission", { project_id: text() diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 0183a9d3df..53a52af331 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -11,8 +11,7 @@ import { Snapshot } from "@/snapshot" import { Log } from "@/util/log" import path from "path" import { Instance } from "@/project/instance" -import { Database, eq } from "@/storage/db" -import { SessionDiffTable } from "./session.sql" +import { Storage } from "@/storage/storage" import { Bus } from "@/bus" import { LLM } from "./llm" @@ -56,13 +55,7 @@ export namespace SessionSummary { files: diffs.length, }, }) - Database.use((db) => - db - .insert(SessionDiffTable) - .values({ session_id: input.sessionID, data: diffs }) - .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } }) - .run(), - ) + await Storage.write(["session_diff", input.sessionID], diffs) Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs, @@ -124,10 +117,11 @@ export namespace SessionSummary { messageID: Identifier.schema("message").optional(), }), async (input) => { - const row = Database.use((db) => - db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, input.sessionID)).get(), - ) - return row?.data ?? [] + try { + return await Storage.read(["session_diff", input.sessionID]) + } catch { + return [] + } }, ) diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 03bbcc148e..6ef7cbaaf5 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" -import { Database, eq } from "../storage/db" +import { Database, eq, asc } from "../storage/db" import { TodoTable } from "./session.sql" export namespace Todo { @@ -26,18 +26,34 @@ export namespace Todo { } export function update(input: { sessionID: string; todos: Info[] }) { - Database.use((db) => - db - .insert(TodoTable) - .values({ session_id: input.sessionID, data: input.todos }) - .onConflictDoUpdate({ target: TodoTable.session_id, set: { data: input.todos } }) - .run(), - ) + Database.transaction((db) => { + db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() + if (input.todos.length === 0) return + db.insert(TodoTable) + .values( + input.todos.map((todo, position) => ({ + session_id: input.sessionID, + id: todo.id, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + })), + ) + .run() + }) Bus.publish(Event.Updated, input) } export function get(sessionID: string) { - const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).get()) - return row?.data ?? [] + const rows = Database.use((db) => + db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(), + ) + return rows.map((row) => ({ + id: row.id, + content: row.content, + status: row.status, + priority: row.priority, + })) } } diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 0cf978930e..108db444c8 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -81,8 +81,11 @@ export namespace ShareNext { Database.use((db) => db .insert(SessionShareTable) - .values({ session_id: sessionID, data: result }) - .onConflictDoUpdate({ target: SessionShareTable.session_id, set: { data: result } }) + .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) + .onConflictDoUpdate({ + target: SessionShareTable.session_id, + set: { id: result.id, secret: result.secret, url: result.url }, + }) .run(), ) fullSync(sessionID) @@ -93,7 +96,8 @@ export namespace ShareNext { const row = Database.use((db) => db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), ) - return row?.data + if (!row) return + return { id: row.id, secret: row.secret, url: row.url } } type Data = diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index bf8a190461..4d9c9290a5 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,19 +1,11 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" -import type { Session } from "../session" export const SessionShareTable = sqliteTable("session_share", { session_id: text() .primaryKey() .references(() => SessionTable.id, { onDelete: "cascade" }), - data: text({ mode: "json" }).notNull().$type<{ - id: string - secret: string - url: string - }>(), -}) - -export const ShareTable = sqliteTable("share", { - session_id: text().primaryKey(), - data: text({ mode: "json" }).notNull().$type(), + id: text().notNull(), + secret: text().notNull(), + url: text().notNull(), }) diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts deleted file mode 100644 index f7bf4b3fa5..0000000000 --- a/packages/opencode/src/share/share.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Bus } from "../bus" -import { Installation } from "../installation" -import { Session } from "../session" -import { MessageV2 } from "../session/message-v2" -import { Log } from "../util/log" - -export namespace Share { - const log = Log.create({ service: "share" }) - - let queue: Promise = Promise.resolve() - const pending = new Map() - - export async function sync(key: string, content: any) { - if (disabled) return - const [root, ...splits] = key.split("/") - if (root !== "session") return - const [sub, sessionID] = splits - if (sub === "share") return - const share = await Session.getShare(sessionID).catch(() => {}) - if (!share) return - const { secret } = share - pending.set(key, content) - queue = queue - .then(async () => { - const content = pending.get(key) - if (content === undefined) return - pending.delete(key) - - return fetch(`${URL}/share_sync`, { - method: "POST", - body: JSON.stringify({ - sessionID: sessionID, - secret, - key: key, - content, - }), - }) - }) - .then((x) => { - if (x) { - log.info("synced", { - key: key, - status: x.status, - }) - } - }) - } - - export function init() { - Bus.subscribe(Session.Event.Updated, async (evt) => { - await sync("session/info/" + evt.properties.info.id, evt.properties.info) - }) - Bus.subscribe(MessageV2.Event.Updated, async (evt) => { - await sync("session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id, evt.properties.info) - }) - Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { - await sync( - "session/part/" + - evt.properties.part.sessionID + - "/" + - evt.properties.part.messageID + - "/" + - evt.properties.part.id, - evt.properties.part, - ) - }) - } - - export const URL = - process.env["OPENCODE_API"] ?? - (Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai") - - const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" - - export async function create(sessionID: string) { - if (disabled) return { url: "", secret: "" } - return fetch(`${URL}/share_create`, { - method: "POST", - body: JSON.stringify({ sessionID: sessionID }), - }) - .then((x) => x.json()) - .then((x) => x as { url: string; secret: string }) - } - - export async function remove(sessionID: string, secret: string) { - if (disabled) return {} - return fetch(`${URL}/share_delete`, { - method: "POST", - body: JSON.stringify({ sessionID, secret }), - }).then((x) => x.json()) - } -} diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index f49028a18b..0beddca8f2 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -7,11 +7,12 @@ import { Context } from "../util/context" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util/log" -import { migrateFromJson } from "./json-migration" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" -import { readFileSync } from "fs" +import { readFileSync, readdirSync } from "fs" +import fs from "fs/promises" +import { Instance } from "@/project/instance" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined @@ -31,21 +32,39 @@ export namespace Database { type Journal = { sql: string; timestamp: number }[] - function journal(dir: string): Journal { - const file = path.join(dir, "meta/_journal.json") - if (!Bun.file(file).size) return [] - - const data = JSON.parse(readFileSync(file, "utf-8")) as { - entries: { tag: string; when: number }[] - } - - return data.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) + function time(tag: string) { + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) + if (!match) return 0 + return Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) } - const client = lazy(() => { + function migrations(dir: string): Journal { + const dirs = readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + + const sql = dirs + .map((name) => { + const file = path.join(dir, name, "migration.sql") + if (!Bun.file(file).size) return + return { + sql: readFileSync(file, "utf-8"), + timestamp: time(name), + } + }) + .filter(Boolean) as Journal + + return sql.sort((a, b) => a.timestamp - b.timestamp) + } + + export const Client = lazy(() => { log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true }) @@ -62,7 +81,7 @@ export namespace Database { const entries = typeof OPENCODE_MIGRATIONS !== "undefined" ? OPENCODE_MIGRATIONS - : journal(path.join(import.meta.dirname, "../../migration")) + : migrations(path.join(import.meta.dirname, "../../migration")) if (entries.length > 0) { log.info("applying migrations", { count: entries.length, @@ -71,19 +90,6 @@ export namespace Database { migrate(db, entries) } - // Run json migration if not already done - if (!sqlite.prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = 'json-migration'").get()) { - Bun.file(path.join(Global.Path.data, "storage/project")) - .exists() - .then((exists) => { - if (!exists) return - return migrateFromJson(sqlite).then(() => { - sqlite.run("INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('json-migration', ?)", [Date.now()]) - }) - }) - .catch((e) => log.error("json migration failed", { error: e })) - } - return db }) @@ -100,7 +106,7 @@ export namespace Database { } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const result = ctx.provide({ effects, tx: client() }, () => callback(client())) + const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) for (const effect of effects) effect() return result } @@ -122,7 +128,7 @@ export namespace Database { } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const result = client().transaction((tx) => { + const result = Client().transaction((tx) => { return ctx.provide({ tx, effects }, () => callback(tx)) }) for (const effect of effects) effect() diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 4936387fd7..4b235a9dc0 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -1,51 +1,70 @@ import { Database } from "bun:sqlite" import { drizzle } from "drizzle-orm/bun-sqlite" -import { eq } from "drizzle-orm" import { Global } from "../global" import { Log } from "../util/log" import { ProjectTable } from "../project/project.sql" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../session/session.sql" -import { SessionShareTable, ShareTable } from "../share/share.sql" +import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" +import { SessionShareTable } from "../share/share.sql" import path from "path" -const log = Log.create({ service: "json-migration" }) +export namespace JsonMigration { + const log = Log.create({ service: "json-migration" }) -export async function migrateFromJson(sqlite: Database, customStorageDir?: string) { - const storageDir = customStorageDir ?? path.join(Global.Path.data, "storage") + export async function run(sqlite: Database) { + const storageDir = path.join(Global.Path.data, "storage") - log.info("starting json to sqlite migration", { storageDir }) + log.info("starting json to sqlite migration", { storageDir }) - const db = drizzle({ client: sqlite }) - const stats = { - projects: 0, - sessions: 0, - messages: 0, - parts: 0, - diffs: 0, - todos: 0, - permissions: 0, - shares: 0, - errors: [] as string[], - } + const db = drizzle({ client: sqlite }) + const stats = { + projects: 0, + sessions: 0, + messages: 0, + parts: 0, + todos: 0, + permissions: 0, + shares: 0, + errors: [] as string[], + } - // Migrate projects first (no FK deps) - const projectGlob = new Bun.Glob("project/*.json") - for await (const file of projectGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id) { - stats.errors.push(`project missing id: ${file}`) - continue + const limit = 32 + + async function list(pattern: string) { + const items: string[] = [] + const scan = new Bun.Glob(pattern) + for await (const file of scan.scan({ cwd: storageDir, absolute: true })) { + items.push(file) } - db.insert(ProjectTable) - .values({ + return items + } + + async function read(files: string[]) { + const results = await Promise.allSettled(files.map((file) => Bun.file(file).json())) + const items: { file: string; data: any }[] = [] + for (let i = 0; i < results.length; i++) { + const result = results[i] + const file = files[i] + if (result.status === "fulfilled") { + items.push({ file, data: result.value }) + continue + } + stats.errors.push(`failed to read ${file}: ${result.reason}`) + } + return items + } + + // Migrate projects first (no FK deps) + const projectFiles = await list("project/*.json") + for (let i = 0; i < projectFiles.length; i += limit) { + const batch = await read(projectFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + if (!data?.id) { + stats.errors.push(`project missing id: ${item.file}`) + continue + } + values.push({ id: data.id, worktree: data.worktree ?? "/", vcs: data.vcs, @@ -57,32 +76,36 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin time_initialized: data.time?.initialized, sandboxes: data.sandboxes ?? [], }) - .onConflictDoNothing() - .run() - stats.projects++ - } catch (e) { - stats.errors.push(`failed to migrate project ${file}: ${e}`) + } + if (values.length === 0) continue + try { + db.insert(ProjectTable).values(values).onConflictDoNothing().run() + stats.projects += values.length + } catch (e) { + stats.errors.push(`failed to migrate project batch: ${e}`) + } } - } - log.info("migrated projects", { count: stats.projects }) + log.info("migrated projects", { count: stats.projects }) - // Migrate sessions (depends on projects) - const sessionGlob = new Bun.Glob("session/*/*.json") - for await (const file of sessionGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.projectID) { - stats.errors.push(`session missing id or projectID: ${file}`) - continue - } - // Check if project exists (skip orphaned sessions) - const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, data.projectID)).get() - if (!project) { - log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID }) - continue - } - db.insert(SessionTable) - .values({ + const projectRows = db.select({ id: ProjectTable.id }).from(ProjectTable).all() + const projectIds = new Set(projectRows.map((item) => item.id)) + + // Migrate sessions (depends on projects) + const sessionFiles = await list("session/*/*.json") + for (let i = 0; i < sessionFiles.length; i += limit) { + const batch = await read(sessionFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + if (!data?.id || !data?.projectID) { + stats.errors.push(`session missing id or projectID: ${item.file}`) + continue + } + if (!projectIds.has(data.projectID)) { + log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID }) + continue + } + values.push({ id: data.id, project_id: data.projectID, parent_id: data.parentID ?? null, @@ -105,181 +128,199 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin time_compacting: data.time?.compacting ?? null, time_archived: data.time?.archived ?? null, }) - .onConflictDoNothing() - .run() - stats.sessions++ - } catch (e) { - stats.errors.push(`failed to migrate session ${file}: ${e}`) - } - } - log.info("migrated sessions", { count: stats.sessions }) - - // Migrate messages (depends on sessions) - const messageGlob = new Bun.Glob("message/*/*.json") - for await (const file of messageGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.sessionID) { - stats.errors.push(`message missing id or sessionID: ${file}`) - continue } - // Check if session exists - const session = db.select().from(SessionTable).where(eq(SessionTable.id, data.sessionID)).get() - if (!session) { - log.warn("skipping orphaned message", { messageID: data.id, sessionID: data.sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(SessionTable).values(values).onConflictDoNothing().run() + stats.sessions += values.length + } catch (e) { + stats.errors.push(`failed to migrate session batch: ${e}`) } - db.insert(MessageTable) - .values({ - id: data.id, - session_id: data.sessionID, - created_at: data.time?.created ?? Date.now(), - data, - }) - .onConflictDoNothing() - .run() - stats.messages++ - } catch (e) { - stats.errors.push(`failed to migrate message ${file}: ${e}`) } - } - log.info("migrated messages", { count: stats.messages }) + log.info("migrated sessions", { count: stats.sessions }) - // Migrate parts (depends on messages) - const partGlob = new Bun.Glob("part/*/*.json") - for await (const file of partGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - if (!data.id || !data.messageID || !data.sessionID) { - stats.errors.push(`part missing id, messageID, or sessionID: ${file}`) - continue + const sessionRows = db.select({ id: SessionTable.id }).from(SessionTable).all() + const sessionIds = new Set(sessionRows.map((item) => item.id)) + + // Migrate messages + parts per session + const sessionList = Array.from(sessionIds) + for (let i = 0; i < sessionList.length; i += limit) { + const batch = sessionList.slice(i, i + limit) + await Promise.allSettled( + batch.map(async (sessionID) => { + const messageFiles = await list(`message/${sessionID}/*.json`) + const messageIds = new Set() + for (let j = 0; j < messageFiles.length; j += limit) { + const chunk = await read(messageFiles.slice(j, j + limit)) + const values = [] as any[] + for (const item of chunk) { + const data = item.data + if (!data?.id) { + stats.errors.push(`message missing id: ${item.file}`) + continue + } + values.push({ + id: data.id, + session_id: sessionID, + created_at: data.time?.created ?? Date.now(), + data, + }) + messageIds.add(data.id) + } + if (values.length === 0) continue + try { + db.insert(MessageTable).values(values).onConflictDoNothing().run() + stats.messages += values.length + } catch (e) { + stats.errors.push(`failed to migrate message batch: ${e}`) + } + } + + const messageList = Array.from(messageIds) + for (let j = 0; j < messageList.length; j += limit) { + const messageBatch = messageList.slice(j, j + limit) + await Promise.allSettled( + messageBatch.map(async (messageID) => { + const partFiles = await list(`part/${messageID}/*.json`) + for (let k = 0; k < partFiles.length; k += limit) { + const chunk = await read(partFiles.slice(k, k + limit)) + const values = [] as any[] + for (const item of chunk) { + const data = item.data + if (!data?.id || !data?.messageID) { + stats.errors.push(`part missing id or messageID: ${item.file}`) + continue + } + values.push({ + id: data.id, + message_id: data.messageID, + session_id: sessionID, + data, + }) + } + if (values.length === 0) continue + try { + db.insert(PartTable).values(values).onConflictDoNothing().run() + stats.parts += values.length + } catch (e) { + stats.errors.push(`failed to migrate part batch: ${e}`) + } + } + }), + ) + } + }), + ) + } + log.info("migrated messages", { count: stats.messages }) + log.info("migrated parts", { count: stats.parts }) + + // Migrate todos + const todoFiles = await list("todo/*.json") + for (let i = 0; i < todoFiles.length; i += limit) { + const batch = await read(todoFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const sessionID = path.basename(item.file, ".json") + if (!sessionIds.has(sessionID)) { + log.warn("skipping orphaned todo", { sessionID }) + continue + } + if (!Array.isArray(data)) { + stats.errors.push(`todo not an array: ${item.file}`) + continue + } + for (let position = 0; position < data.length; position++) { + const todo = data[position] + if (!todo?.id || !todo?.content || !todo?.status || !todo?.priority) continue + values.push({ + session_id: sessionID, + id: todo.id, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + }) + } } - // Check if message exists - const message = db.select().from(MessageTable).where(eq(MessageTable.id, data.messageID)).get() - if (!message) { - log.warn("skipping orphaned part", { partID: data.id, messageID: data.messageID }) - continue + if (values.length === 0) continue + try { + db.insert(TodoTable).values(values).onConflictDoNothing().run() + stats.todos += values.length + } catch (e) { + stats.errors.push(`failed to migrate todo batch: ${e}`) } - db.insert(PartTable) - .values({ - id: data.id, - message_id: data.messageID, - session_id: data.sessionID, - data, - }) - .onConflictDoNothing() - .run() - stats.parts++ - } catch (e) { - stats.errors.push(`failed to migrate part ${file}: ${e}`) } - } - log.info("migrated parts", { count: stats.parts }) + log.info("migrated todos", { count: stats.todos }) - // Migrate session diffs - const diffGlob = new Bun.Glob("session_diff/*.json") - for await (const file of diffGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - // Check if session exists - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned session_diff", { sessionID }) - continue + // Migrate permissions + const permFiles = await list("permission/*.json") + for (let i = 0; i < permFiles.length; i += limit) { + const batch = await read(permFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const projectID = path.basename(item.file, ".json") + if (!projectIds.has(projectID)) { + log.warn("skipping orphaned permission", { projectID }) + continue + } + values.push({ project_id: projectID, data }) } - db.insert(SessionDiffTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.diffs++ - } catch (e) { - stats.errors.push(`failed to migrate session_diff ${file}: ${e}`) - } - } - log.info("migrated session diffs", { count: stats.diffs }) - - // Migrate todos - const todoGlob = new Bun.Glob("todo/*.json") - for await (const file of todoGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned todo", { sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(PermissionTable).values(values).onConflictDoNothing().run() + stats.permissions += values.length + } catch (e) { + stats.errors.push(`failed to migrate permission batch: ${e}`) } - db.insert(TodoTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.todos++ - } catch (e) { - stats.errors.push(`failed to migrate todo ${file}: ${e}`) } - } - log.info("migrated todos", { count: stats.todos }) + log.info("migrated permissions", { count: stats.permissions }) - // Migrate permissions - const permGlob = new Bun.Glob("permission/*.json") - for await (const file of permGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const projectID = path.basename(file, ".json") - const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get() - if (!project) { - log.warn("skipping orphaned permission", { projectID }) - continue + // Migrate session shares + const shareFiles = await list("session_share/*.json") + for (let i = 0; i < shareFiles.length; i += limit) { + const batch = await read(shareFiles.slice(i, i + limit)) + const values = [] as any[] + for (const item of batch) { + const data = item.data + const sessionID = path.basename(item.file, ".json") + if (!sessionIds.has(sessionID)) { + log.warn("skipping orphaned session_share", { sessionID }) + continue + } + if (!data?.id || !data?.secret || !data?.url) { + stats.errors.push(`session_share missing id/secret/url: ${item.file}`) + continue + } + values.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url }) } - db.insert(PermissionTable).values({ project_id: projectID, data }).onConflictDoNothing().run() - stats.permissions++ - } catch (e) { - stats.errors.push(`failed to migrate permission ${file}: ${e}`) - } - } - log.info("migrated permissions", { count: stats.permissions }) - - // Migrate session shares - const shareGlob = new Bun.Glob("session_share/*.json") - for await (const file of shareGlob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get() - if (!session) { - log.warn("skipping orphaned session_share", { sessionID }) - continue + if (values.length === 0) continue + try { + db.insert(SessionShareTable).values(values).onConflictDoNothing().run() + stats.shares += values.length + } catch (e) { + stats.errors.push(`failed to migrate session_share batch: ${e}`) } - db.insert(SessionShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - stats.shares++ - } catch (e) { - stats.errors.push(`failed to migrate session_share ${file}: ${e}`) } - } - log.info("migrated session shares", { count: stats.shares }) + log.info("migrated session shares", { count: stats.shares }) - // Migrate shares (downloaded shared sessions, no FK) - const share2Glob = new Bun.Glob("share/*.json") - for await (const file of share2Glob.scan({ cwd: storageDir, absolute: true })) { - try { - const data = await Bun.file(file).json() - const sessionID = path.basename(file, ".json") - db.insert(ShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run() - } catch (e) { - stats.errors.push(`failed to migrate share ${file}: ${e}`) + log.info("json migration complete", { + projects: stats.projects, + sessions: stats.sessions, + messages: stats.messages, + parts: stats.parts, + todos: stats.todos, + permissions: stats.permissions, + shares: stats.shares, + errorCount: stats.errors.length, + }) + + if (stats.errors.length > 0) { + log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) } + + return stats } - - log.info("json migration complete", { - projects: stats.projects, - sessions: stats.sessions, - messages: stats.messages, - parts: stats.parts, - diffs: stats.diffs, - todos: stats.todos, - permissions: stats.permissions, - shares: stats.shares, - errorCount: stats.errors.length, - }) - - if (stats.errors.length > 0) { - log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) - } - - return stats } diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts new file mode 100644 index 0000000000..18f2d67e7a --- /dev/null +++ b/packages/opencode/src/storage/storage.ts @@ -0,0 +1,227 @@ +import { Log } from "../util/log" +import path from "path" +import fs from "fs/promises" +import { Global } from "../global" +import { Filesystem } from "../util/filesystem" +import { lazy } from "../util/lazy" +import { Lock } from "../util/lock" +import { $ } from "bun" +import { NamedError } from "@opencode-ai/util/error" +import z from "zod" + +export namespace Storage { + const log = Log.create({ service: "storage" }) + + type Migration = (dir: string) => Promise + + export const NotFoundError = NamedError.create( + "NotFoundError", + z.object({ + message: z.string(), + }), + ) + + const MIGRATIONS: Migration[] = [ + async (dir) => { + const project = path.resolve(dir, "../project") + if (!(await Filesystem.isDir(project))) return + for await (const projectDir of new Bun.Glob("*").scan({ + cwd: project, + onlyFiles: false, + })) { + log.info(`migrating project ${projectDir}`) + let projectID = projectDir + const fullProjectDir = path.join(project, projectDir) + let worktree = "/" + + if (projectID !== "global") { + for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ + cwd: path.join(project, projectDir), + absolute: true, + })) { + const json = await Bun.file(msgFile).json() + worktree = json.path?.root + if (worktree) break + } + if (!worktree) continue + if (!(await Filesystem.isDir(worktree))) continue + const [id] = await $`git rev-list --max-parents=0 --all` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => + x + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted(), + ) + if (!id) continue + projectID = id + + await Bun.write( + path.join(dir, "project", projectID + ".json"), + JSON.stringify({ + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }), + ) + + log.info(`migrating sessions for project ${projectID}`) + for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "session", projectID, path.basename(sessionFile)) + log.info("copying", { + sessionFile, + dest, + }) + const session = await Bun.file(sessionFile).json() + await Bun.write(dest, JSON.stringify(session)) + log.info(`migrating messages for session ${session.id}`) + for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "message", session.id, path.basename(msgFile)) + log.info("copying", { + msgFile, + dest, + }) + const message = await Bun.file(msgFile).json() + await Bun.write(dest, JSON.stringify(message)) + + log.info(`migrating parts for message ${message.id}`) + for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( + { + cwd: fullProjectDir, + absolute: true, + }, + )) { + const dest = path.join(dir, "part", message.id, path.basename(partFile)) + const part = await Bun.file(partFile).json() + log.info("copying", { + partFile, + dest, + }) + await Bun.write(dest, JSON.stringify(part)) + } + } + } + } + } + }, + async (dir) => { + for await (const item of new Bun.Glob("session/*/*.json").scan({ + cwd: dir, + absolute: true, + })) { + const session = await Bun.file(item).json() + if (!session.projectID) continue + if (!session.summary?.diffs) continue + const { diffs } = session.summary + await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs)) + await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write( + JSON.stringify({ + ...session, + summary: { + additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), + deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), + }, + }), + ) + } + }, + ] + + const state = lazy(async () => { + const dir = path.join(Global.Path.data, "storage") + const migration = await Bun.file(path.join(dir, "migration")) + .json() + .then((x) => parseInt(x)) + .catch(() => 0) + for (let index = migration; index < MIGRATIONS.length; index++) { + log.info("running migration", { index }) + const migration = MIGRATIONS[index] + await migration(dir).catch(() => log.error("failed to run migration", { index })) + await Bun.write(path.join(dir, "migration"), (index + 1).toString()) + } + return { + dir, + } + }) + + export async function remove(key: string[]) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + await fs.unlink(target).catch(() => {}) + }) + } + + export async function read(key: string[]) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.read(target) + const result = await Bun.file(target).json() + return result as T + }) + } + + export async function update(key: string[], fn: (draft: T) => void) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.write(target) + const content = await Bun.file(target).json() + fn(content) + await Bun.write(target, JSON.stringify(content, null, 2)) + return content as T + }) + } + + export async function write(key: string[], content: T) { + const dir = await state().then((x) => x.dir) + const target = path.join(dir, ...key) + ".json" + return withErrorHandling(async () => { + using _ = await Lock.write(target) + await Bun.write(target, JSON.stringify(content, null, 2)) + }) + } + + async function withErrorHandling(body: () => Promise) { + return body().catch((e) => { + if (!(e instanceof Error)) throw e + const errnoException = e as NodeJS.ErrnoException + if (errnoException.code === "ENOENT") { + throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` }) + } + throw e + }) + } + + const glob = new Bun.Glob("**/*") + export async function list(prefix: string[]) { + const dir = await state().then((x) => x.dir) + try { + const result = await Array.fromAsync( + glob.scan({ + cwd: path.join(dir, ...prefix), + onlyFiles: true, + }), + ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) + result.sort() + return result + } catch { + return [] + } + } +} diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 70582eed04..bc55124002 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -5,20 +5,12 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator" import { eq } from "drizzle-orm" import path from "path" import fs from "fs/promises" -import { readFileSync } from "fs" -import os from "os" -import { migrateFromJson } from "../../src/storage/json-migration" +import { readFileSync, readdirSync } from "fs" +import { JsonMigration } from "../../src/storage/json-migration" +import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" import { Project } from "../../src/project/project" -import { - SessionTable, - MessageTable, - PartTable, - SessionDiffTable, - TodoTable, - PermissionTable, -} from "../../src/session/session.sql" -import { SessionShareTable, ShareTable } from "../../src/share/share.sql" +import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" // Test fixtures const fixtures = { @@ -56,8 +48,9 @@ const fixtures = { } // Helper to create test storage directory structure -async function setupStorageDir(baseDir: string) { - const storageDir = path.join(baseDir, "storage") +async function setupStorageDir() { + const storageDir = path.join(Global.Path.data, "storage") + await fs.rm(storageDir, { recursive: true, force: true }) await fs.mkdir(path.join(storageDir, "project"), { recursive: true }) await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true }) await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true }) @@ -66,7 +59,6 @@ async function setupStorageDir(baseDir: string) { await fs.mkdir(path.join(storageDir, "todo"), { recursive: true }) await fs.mkdir(path.join(storageDir, "permission"), { recursive: true }) await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true }) - await fs.mkdir(path.join(storageDir, "share"), { recursive: true }) // Create legacy marker to indicate JSON storage exists await Bun.write(path.join(storageDir, "migration"), "1") return storageDir @@ -79,33 +71,31 @@ function createTestDb() { // Apply schema migrations using drizzle migrate const dir = path.join(import.meta.dirname, "../../migration") - const journal = JSON.parse(readFileSync(path.join(dir, "meta/_journal.json"), "utf-8")) as { - entries: { tag: string; when: number }[] - } - const migrations = journal.entries.map((entry) => ({ - sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"), - timestamp: entry.when, - })) + const entries = readdirSync(dir, { withFileTypes: true }) + const migrations = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => ({ + sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"), + timestamp: Number(entry.name.split("_")[0]), + })) + .sort((a, b) => a.timestamp - b.timestamp) migrate(drizzle({ client: sqlite }), migrations) return sqlite } describe("JSON to SQLite migration", () => { - let tmpDir: string let storageDir: string let sqlite: Database beforeEach(async () => { - tmpDir = path.join(os.tmpdir(), "opencode-migration-test-" + Math.random().toString(36).slice(2)) - await fs.mkdir(tmpDir, { recursive: true }) - storageDir = await setupStorageDir(tmpDir) + storageDir = await setupStorageDir() sqlite = createTestDb() }) afterEach(async () => { sqlite.close() - await fs.rm(tmpDir, { recursive: true, force: true }) + await fs.rm(storageDir, { recursive: true, force: true }) }) test("migrates project", async () => { @@ -121,7 +111,7 @@ describe("JSON to SQLite migration", () => { }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.projects).toBe(1) @@ -161,7 +151,7 @@ describe("JSON to SQLite migration", () => { }), ) - await migrateFromJson(sqlite, storageDir) + await JsonMigration.run(sqlite) const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() @@ -198,7 +188,7 @@ describe("JSON to SQLite migration", () => { JSON.stringify({ ...fixtures.part }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.messages).toBe(1) expect(stats?.parts).toBe(1) @@ -227,7 +217,7 @@ describe("JSON to SQLite migration", () => { }), ) - const stats = await migrateFromJson(sqlite, storageDir) + const stats = await JsonMigration.run(sqlite) expect(stats?.sessions).toBe(0) }) @@ -243,8 +233,8 @@ describe("JSON to SQLite migration", () => { }), ) - await migrateFromJson(sqlite, storageDir) - await migrateFromJson(sqlite, storageDir) + await JsonMigration.run(sqlite) + await JsonMigration.run(sqlite) const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() From f40685ab13610f57ca74c2cb005194e037680740 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 12:38:38 -0500 Subject: [PATCH 11/35] 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 f0f065de4a..99e8612f87 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/35] 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 bc17ef4938..db59eb0213 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 0f5a856e51..76a8ecdc3b 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 4085d99a37..3a18735416 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 7d28b912c5..d5a5d17622 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 2afaef5aa0..1eab195922 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 4d9c9290a5..1dcab7b851 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 0beddca8f2..370fa846c2 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 4b235a9dc0..c1236a0f76 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 bc55124002..d72d442358 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/35] core: switch commit command to kimi-k2.5 and improve worktree test reliability --- .opencode/command/commit.md | 2 +- .../opencode/test/project/project.test.ts | 57 ++++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 8260029195..d8a420b173 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 bee8b77dd1..36eea10231 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 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) + 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() + 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() + 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) + 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() + 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/35] 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 5c0d9bdad5..d06cfe4d98 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 370fa846c2..54a8acc34d 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/35] 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 76a8ecdc3b..7c4dc94cc4 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 1eab195922..ea7c1e32a3 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 1dcab7b851..268d41a6f6 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 54a8acc34d..738e7c6025 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 0000000000..ead3518dee --- /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 d72d442358..09578ebe8c 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/35] sync --- .../migration.sql | 5 +- .../snapshot.json | 138 ++++++++++++------ 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, 143 insertions(+), 57 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 db59eb0213..775c1a1173 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 63943f49d3..1e297d3f8a 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" }, @@ -454,16 +474,6 @@ "entityType": "columns", "table": "todo" }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "todo" - }, { "type": "text", "notNull": true, @@ -504,6 +514,26 @@ "entityType": "columns", "table": "todo" }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, { "type": "text", "notNull": false, @@ -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 ea7c1e32a3..9c5c72c4c5 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 6ef7cbaaf5..ec2bcdda3c 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 c1236a0f76..0debe2e2e9 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 09578ebe8c..f9be0d8041 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 00e60899cc7222a6ddae08fef5050d80468d1d50 Mon Sep 17 00:00:00 2001 From: Github Action Date: Wed, 28 Jan 2026 23:49:18 +0000 Subject: [PATCH 17/35] 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 0b735b35d6..9d2bb999d6 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=" } } From 2e8feb1c78cc82094a870b558706e69c580fb91f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 28 Jan 2026 21:50:38 -0500 Subject: [PATCH 18/35] core: significantly speed up data migration by optimizing SQLite settings and batch processing Reduces migration time from minutes to seconds by enabling WAL mode, increasing batch size to 1000, and pre-scanning files upfront. Users upgrading to the SQLite backend will now see much faster startup times when their existing data is migrated. --- .../opencode/src/storage/json-migration.ts | 390 ++++++++++-------- 1 file changed, 218 insertions(+), 172 deletions(-) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 0debe2e2e9..179d5a064a 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -14,8 +14,15 @@ export namespace JsonMigration { const storageDir = path.join(Global.Path.data, "storage") log.info("starting json to sqlite migration", { storageDir }) + const start = performance.now() const db = drizzle({ client: sqlite }) + + // Optimize SQLite for bulk inserts + sqlite.exec("PRAGMA journal_mode = WAL") + sqlite.exec("PRAGMA synchronous = OFF") + sqlite.exec("PRAGMA cache_size = 10000") + sqlite.exec("PRAGMA temp_store = MEMORY") const stats = { projects: 0, sessions: 0, @@ -26,8 +33,16 @@ export namespace JsonMigration { shares: 0, errors: [] as string[], } + const orphans = { + sessions: 0, + todos: 0, + permissions: 0, + shares: 0, + } + const errs = stats.errors - const limit = 32 + const batchSize = 1000 + const now = Date.now() async function list(pattern: string) { const items: string[] = [] @@ -38,74 +53,112 @@ export namespace JsonMigration { return items } - async function read(files: string[]) { - const results = await Promise.allSettled(files.map((file) => Bun.file(file).json())) - const items: { file: string; data: any }[] = [] + async function read(files: string[], start: number, end: number) { + const count = end - start + const tasks = new Array(count) + for (let i = 0; i < count; i++) { + tasks[i] = Bun.file(files[start + i]).json() + } + const results = await Promise.allSettled(tasks) + const items = new Array(count) 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 }) + items[i] = result.value continue } - stats.errors.push(`failed to read ${file}: ${result.reason}`) + errs.push(`failed to read ${files[start + i]}: ${result.reason}`) } return items } + function insert(values: any[], table: any, label: string) { + if (values.length === 0) return 0 + try { + db.insert(table).values(values).onConflictDoNothing().run() + return values.length + } catch (e) { + errs.push(`failed to migrate ${label} batch: ${e}`) + return 0 + } + } + + // Pre-scan all files upfront to avoid repeated glob operations + log.info("scanning files...") + const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([ + list("project/*.json"), + list("session/*/*.json"), + list("message/*/*.json"), + list("part/*/*.json"), + list("todo/*.json"), + list("permission/*.json"), + list("session_share/*.json"), + ]) + + log.info("file scan complete", { + projects: projectFiles.length, + sessions: sessionFiles.length, + messages: messageFiles.length, + parts: partFiles.length, + todos: todoFiles.length, + permissions: permFiles.length, + shares: shareFiles.length, + }) + + sqlite.exec("BEGIN TRANSACTION") + // 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 + const projectIds = new Set() + const projectValues = [] as any[] + for (let i = 0; i < projectFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, projectFiles.length) + const batch = await read(projectFiles, i, end) + projectValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue if (!data?.id) { - stats.errors.push(`project missing id: ${item.file}`) + errs.push(`project missing id: ${projectFiles[i + j]}`) continue } - values.push({ + projectIds.add(data.id) + projectValues.push({ 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_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, time_initialized: data.time?.initialized, sandboxes: data.sandboxes ?? [], }) } - 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}`) - } + stats.projects += insert(projectValues, ProjectTable, "project") } - 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)) + log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) }) // 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 + const sessionIds = new Set() + const sessionValues = [] as any[] + for (let i = 0; i < sessionFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, sessionFiles.length) + const batch = await read(sessionFiles, i, end) + sessionValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue if (!data?.id || !data?.projectID) { - stats.errors.push(`session missing id or projectID: ${item.file}`) + errs.push(`session missing id or projectID: ${sessionFiles[i + j]}`) continue } if (!projectIds.has(data.projectID)) { - log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID }) + orphans.sessions++ continue } - values.push({ + sessionIds.add(data.id) + sessionValues.push({ id: data.id, project_id: data.projectID, parent_id: data.parentID ?? null, @@ -120,117 +173,110 @@ export namespace JsonMigration { summary_diffs: data.summary?.diffs ?? null, revert: data.revert ?? null, permission: data.permission ?? null, - time_created: data.time?.created ?? Date.now(), - time_updated: data.time?.updated ?? Date.now(), + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, time_compacting: data.time?.compacting ?? null, time_archived: data.time?.archived ?? null, }) } - 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}`) - } + stats.sessions += insert(sessionValues, SessionTable, "session") } log.info("migrated sessions", { count: stats.sessions }) + if (orphans.sessions > 0) { + log.warn("skipped orphaned sessions", { count: orphans.sessions }) + } - const sessionRows = db.select({ id: SessionTable.id }).from(SessionTable).all() - const sessionIds = new Set(sessionRows.map((item) => item.id)) + // Migrate messages using pre-scanned file map + const allMessageFiles = [] as string[] + const allMessageSessions = [] as string[] + for (const file of messageFiles) { + const sessionID = path.basename(path.dirname(file)) + if (!sessionIds.has(sessionID)) continue + allMessageFiles.push(file) + allMessageSessions.push(sessionID) + } - // 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 - } - const { id, sessionID: _, ...rest } = data - values.push({ - id: data.id, - session_id: sessionID, - time_created: data.time?.created ?? Date.now(), - time_updated: data.time?.updated ?? Date.now(), - data: rest, - }) - 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 - } - const { id, messageID, sessionID: _, ...rest } = data - values.push({ - id: data.id, - message_id: data.messageID, - session_id: sessionID, - time_created: data.time?.created ?? Date.now(), - time_updated: data.time?.updated ?? Date.now(), - data: rest, - }) - } - 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}`) - } - } - }), - ) - } - }), - ) + for (let i = 0; i < allMessageFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, allMessageFiles.length) + const batch = await read(allMessageFiles, i, end) + const values = new Array(batch.length) + let count = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + if (!data?.id) { + errs.push(`message missing id: ${allMessageFiles[i + j]}`) + continue + } + const sessionID = allMessageSessions[i + j] + const id = data.id + const rest = data + delete rest.id + delete rest.sessionID + values[count++] = { + id, + session_id: sessionID, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + data: rest, + } + } + values.length = count + stats.messages += insert(values, MessageTable, "message") } log.info("migrated messages", { count: stats.messages }) + + // Migrate parts using pre-scanned file map + for (let i = 0; i < partFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, partFiles.length) + const batch = await read(partFiles, i, end) + const values = new Array(batch.length) + let count = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + if (!data?.id || !data?.messageID || !data?.sessionID) { + errs.push(`part missing id/messageID/sessionID: ${partFiles[i + j]}`) + continue + } + if (!sessionIds.has(data.sessionID)) continue + const id = data.id + const messageID = data.messageID + const sessionID = data.sessionID + const rest = data + delete rest.id + delete rest.messageID + delete rest.sessionID + values[count++] = { + id, + message_id: messageID, + session_id: sessionID, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + data: rest, + } + } + values.length = count + stats.parts += insert(values, PartTable, "part") + } log.info("migrated parts", { count: stats.parts }) // Migrate todos - const todoFiles = await list("todo/*.json") - for (let i = 0; i < todoFiles.length; i += limit) { - const batch = await read(todoFiles.slice(i, i + limit)) + const todoSessions = todoFiles.map((file) => path.basename(file, ".json")) + for (let i = 0; i < todoFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, todoFiles.length) + const batch = await read(todoFiles, i, end) const values = [] as any[] - for (const item of batch) { - const data = item.data - const sessionID = path.basename(item.file, ".json") + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const sessionID = todoSessions[i + j] if (!sessionIds.has(sessionID)) { - log.warn("skipping orphaned todo", { sessionID }) + orphans.todos++ continue } if (!Array.isArray(data)) { - stats.errors.push(`todo not an array: ${item.file}`) + errs.push(`todo not an array: ${todoFiles[i + j]}`) continue } for (let position = 0; position < data.length; position++) { @@ -242,72 +288,71 @@ export namespace JsonMigration { status: todo.status, priority: todo.priority, position, - time_created: Date.now(), - time_updated: Date.now(), + time_created: now, + time_updated: now, }) } } - 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}`) - } + stats.todos += insert(values, TodoTable, "todo") } log.info("migrated todos", { count: stats.todos }) + if (orphans.todos > 0) { + log.warn("skipped orphaned todos", { count: orphans.todos }) + } // 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") + const permProjects = permFiles.map((file) => path.basename(file, ".json")) + const permValues = [] as any[] + for (let i = 0; i < permFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, permFiles.length) + const batch = await read(permFiles, i, end) + permValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const projectID = permProjects[i + j] if (!projectIds.has(projectID)) { - log.warn("skipping orphaned permission", { projectID }) + orphans.permissions++ 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}`) + permValues.push({ project_id: projectID, data }) } + stats.permissions += insert(permValues, PermissionTable, "permission") } log.info("migrated permissions", { count: stats.permissions }) + if (orphans.permissions > 0) { + log.warn("skipped orphaned permissions", { count: orphans.permissions }) + } // 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") + const shareSessions = shareFiles.map((file) => path.basename(file, ".json")) + const shareValues = [] as any[] + for (let i = 0; i < shareFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, shareFiles.length) + const batch = await read(shareFiles, i, end) + shareValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const sessionID = shareSessions[i + j] if (!sessionIds.has(sessionID)) { - log.warn("skipping orphaned session_share", { sessionID }) + orphans.shares++ continue } if (!data?.id || !data?.secret || !data?.url) { - stats.errors.push(`session_share missing id/secret/url: ${item.file}`) + errs.push(`session_share missing id/secret/url: ${shareFiles[i + j]}`) 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}`) + shareValues.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url }) } + stats.shares += insert(shareValues, SessionShareTable, "session_share") } log.info("migrated session shares", { count: stats.shares }) + if (orphans.shares > 0) { + log.warn("skipped orphaned session shares", { count: orphans.shares }) + } + + sqlite.exec("COMMIT") log.info("json migration complete", { projects: stats.projects, @@ -318,6 +363,7 @@ export namespace JsonMigration { permissions: stats.permissions, shares: stats.shares, errorCount: stats.errors.length, + duration: Math.round(performance.now() - start), }) if (stats.errors.length > 0) { From 1bc45dc266b7e677e66c34ea505b0c46d3fb1892 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 28 Jan 2026 21:52:41 -0500 Subject: [PATCH 19/35] ignore: keep Chinese release notes label from blocking updates --- packages/app/src/i18n/zh.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index ef69855b12..2266c109b0 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -529,9 +529,6 @@ export const dict = { "settings.general.row.releaseNotes.title": "发行说明", "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", - "settings.general.row.releaseNotes.title": "发行说明", - "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", - "settings.updates.row.startup.title": "启动时检查更新", "settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新", "settings.updates.row.check.title": "检查更新", From e3519356f275460181139700048f05df73d8aaa7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 29 Jan 2026 13:32:20 -0500 Subject: [PATCH 20/35] sync From 53b610c331970ed651beb50d99dd77a15be8cf2f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 29 Jan 2026 13:38:47 -0500 Subject: [PATCH 21/35] sync From 3ea499f04ea37efd6eb77bbed095aa9faf51a576 Mon Sep 17 00:00:00 2001 From: Github Action Date: Thu, 29 Jan 2026 20:15:38 +0000 Subject: [PATCH 22/35] 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 8e95135aa2..10320c4684 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=", - "aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=", - "aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=", - "x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8=" + "x86_64-linux": "sha256-Xin761tN/BqAqmICQQXS9tdrc6PuHm8L5h6RcrFQdxY=", + "aarch64-linux": "sha256-Q3aNOeF3MLY+UY8wRM16DmrbRC7R4xQvcty41M/OSMY=", + "aarch64-darwin": "sha256-okAB6LfrRa9Vj98JqwxQApKJ4b0xcLSW0hmD8j234YU=", + "x86_64-darwin": "sha256-vJXxSIk9UTCVduDAvFkgBcGa6rMbY3MJDI4YBCEFAqw=" } } From 9c3e3c1ab53c8a6fa038d821fa8f0441af72c3e6 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 29 Jan 2026 16:02:18 -0500 Subject: [PATCH 23/35] core: load migrations from timestamp-named directories instead of journal --- packages/opencode/script/build.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 637fb983fa..969b3c83bd 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -25,14 +25,28 @@ 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 }[] -} +// Load migrations from migration directories +const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true })) + .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name)) + .map((entry) => entry.name) + .sort() + 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 } + migrationDirs.map(async (name) => { + const file = path.join(dir, "migration", name, "migration.sql") + const sql = await Bun.file(file).text() + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name) + const timestamp = match + ? Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) + : 0 + return { sql, timestamp } }), ) console.log(`Loaded ${migrations.length} migrations`) From 498cbb2c269370ba8a8eeaad836e2eb50f599fe1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 Jan 2026 11:13:16 -0500 Subject: [PATCH 24/35] core: split message part updates into delta events for smoother streaming Streaming text and reasoning content now uses incremental delta events instead of sending full message parts on each update. This reduces bandwidth and improves real-time response smoothness in the TUI. --- packages/app/src/context/global-sync.tsx | 17 ++++ packages/opencode/src/acp/agent.ts | 90 ++++++++++++------- .../opencode/src/cli/cmd/tui/context/sync.tsx | 18 ++++ packages/opencode/src/session/index.ts | 30 +++---- packages/opencode/src/session/message-v2.ts | 11 ++- packages/opencode/src/session/processor.ts | 27 ++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 17 ++-- 7 files changed, 147 insertions(+), 63 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index ad3d124b2c..12fe37acf3 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -839,6 +839,23 @@ function createGlobalSync() { ) break } + case "message.part.delta": { + const parts = store.part[event.properties.messageID] + if (!parts) break + const result = Binary.search(parts, event.properties.partID, (p) => p.id) + if (!result.found) break + setStore( + "part", + event.properties.messageID, + produce((draft) => { + const part = draft[result.index] + const field = event.properties.field as keyof typeof part + const existing = part[field] as string | undefined + ;(part[field] as string) = (existing ?? "") + event.properties.delta + }), + ) + break + } case "message.part.removed": { const messageID = event.properties.messageID const parts = store.part[messageID] diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index cc9a029a04..2af375b996 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -365,46 +365,68 @@ export namespace ACP { return } } + return + } - if (part.type === "text") { - const delta = props.delta - if (delta && part.ignored !== true) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: delta, - }, + case "message.part.delta": { + const props = event.properties + const session = this.sessionManager.tryGet(props.sessionID) + if (!session) return + const sessionId = session.id + + const message = await this.sdk.session + .message( + { + sessionID: props.sessionID, + messageID: props.messageID, + directory: session.cwd, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((error) => { + log.error("unexpected error when fetching message", { error }) + return undefined + }) + + if (!message || message.info.role !== "assistant") return + + const part = message.parts.find((p) => p.id === props.partID) + if (!part) return + + if (part.type === "text" && props.field === "text" && part.ignored !== true) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: props.delta, }, - }) - .catch((error) => { - log.error("failed to send text to ACP", { error }) - }) - } + }, + }) + .catch((error) => { + log.error("failed to send text delta to ACP", { error }) + }) return } - if (part.type === "reasoning") { - const delta = props.delta - if (delta) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text: delta, - }, + if (part.type === "reasoning" && props.field === "text") { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: props.delta, }, - }) - .catch((error) => { - log.error("failed to send reasoning to ACP", { error }) - }) - } + }, + }) + .catch((error) => { + log.error("failed to send reasoning delta to ACP", { error }) + }) } return } diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index eb8ed2d9bb..269ed7ae0b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -299,6 +299,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ break } + case "message.part.delta": { + const parts = store.part[event.properties.messageID] + if (!parts) break + const result = Binary.search(parts, event.properties.partID, (p) => p.id) + if (!result.found) break + setStore( + "part", + event.properties.messageID, + produce((draft) => { + const part = draft[result.index] + const field = event.properties.field as keyof typeof part + const existing = part[field] as string | undefined + ;(part[field] as string) = (existing ?? "") + event.properties.delta + }), + ) + break + } + case "message.part.removed": { const parts = store.part[event.properties.messageID] const result = Binary.search(parts, event.properties.partID, (p) => p.id) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 2d506e2e96..e87e4e4e44 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -603,21 +603,9 @@ export namespace Session { }, ) - const UpdatePartInput = z.union([ - MessageV2.Part, - z.object({ - part: MessageV2.TextPart, - delta: z.string(), - }), - z.object({ - part: MessageV2.ReasoningPart, - delta: z.string(), - }), - ]) + const UpdatePartInput = MessageV2.Part - export const updatePart = fn(UpdatePartInput, async (input) => { - const part = "delta" in input ? input.part : input - const delta = "delta" in input ? input.delta : undefined + export const updatePart = fn(UpdatePartInput, async (part) => { const { id, messageID, sessionID, ...data } = part const time = Date.now() Database.use((db) => { @@ -634,13 +622,25 @@ export namespace Session { Database.effect(() => Bus.publish(MessageV2.Event.PartUpdated, { part, - delta, }), ) }) return part }) + export const updatePartDelta = fn( + z.object({ + sessionID: z.string(), + messageID: z.string(), + partID: z.string(), + field: z.string(), + delta: z.string(), + }), + async (input) => { + Bus.publish(MessageV2.Event.PartDelta, input) + }, + ) + export const getUsage = fn( z.object({ model: z.custom(), diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index f5a4d7abbd..1b6bf04a84 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -417,7 +417,16 @@ export namespace MessageV2 { "message.part.updated", z.object({ part: Part, - delta: z.string().optional(), + }), + ), + PartDelta: BusEvent.define( + "message.part.delta", + z.object({ + sessionID: z.string(), + messageID: z.string(), + partID: z.string(), + field: z.string(), + delta: z.string(), }), ), PartRemoved: BusEvent.define( diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 2707105618..826d0842cc 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -63,17 +63,19 @@ export namespace SessionProcessor { if (value.id in reasoningMap) { continue } - reasoningMap[value.id] = { + const reasoningPart = { id: Identifier.ascending("part"), messageID: input.assistantMessage.id, sessionID: input.assistantMessage.sessionID, - type: "reasoning", + type: "reasoning" as const, text: "", time: { start: Date.now(), }, metadata: value.providerMetadata, } + reasoningMap[value.id] = reasoningPart + await Session.updatePart(reasoningPart) break case "reasoning-delta": @@ -81,7 +83,13 @@ export namespace SessionProcessor { const part = reasoningMap[value.id] part.text += value.text if (value.providerMetadata) part.metadata = value.providerMetadata - if (part.text) await Session.updatePart({ part, delta: value.text }) + await Session.updatePartDelta({ + sessionID: part.sessionID, + messageID: part.messageID, + partID: part.id, + field: "text", + delta: value.text, + }) } break @@ -288,17 +296,20 @@ export namespace SessionProcessor { }, metadata: value.providerMetadata, } + await Session.updatePart(currentText) break case "text-delta": if (currentText) { currentText.text += value.text if (value.providerMetadata) currentText.metadata = value.providerMetadata - if (currentText.text) - await Session.updatePart({ - part: currentText, - delta: value.text, - }) + await Session.updatePartDelta({ + sessionID: currentText.sessionID, + messageID: currentText.messageID, + partID: currentText.id, + field: "text", + delta: value.text, + }) } break diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8555e84384..20fc5920df 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -480,7 +480,17 @@ export type EventMessagePartUpdated = { type: "message.part.updated" properties: { part: Part - delta?: string + } +} + +export type EventMessagePartDelta = { + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string } } @@ -650,10 +660,6 @@ export type Todo = { * Priority level of the task: high, medium, low */ priority: string - /** - * Unique identifier for the todo item - */ - id: string } export type EventTodoUpdated = { @@ -896,6 +902,7 @@ export type Event = | EventMessageUpdated | EventMessageRemoved | EventMessagePartUpdated + | EventMessagePartDelta | EventMessagePartRemoved | EventPermissionAsked | EventPermissionReplied From b8cbfd48ec9ba0ca4f0ad29a746aeb51bc33d5a7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 Jan 2026 11:17:33 -0500 Subject: [PATCH 25/35] format --- .../snapshot.json | 83 +++++-------------- packages/sdk/openapi.json | 39 +++++++-- 2 files changed, 54 insertions(+), 68 deletions(-) diff --git a/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json b/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json index 1e297d3f8a..ff76ee209a 100644 --- a/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json +++ b/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json @@ -2,9 +2,7 @@ "version": "7", "dialect": "sqlite", "id": "068758ed-a97a-46f6-8a59-6c639ae7c20c", - "prevIds": [ - "00000000-0000-0000-0000-000000000000" - ], + "prevIds": ["00000000-0000-0000-0000-000000000000"], "ddl": [ { "name": "project", @@ -595,13 +593,9 @@ "table": "session_share" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -610,13 +604,9 @@ "table": "message" }, { - "columns": [ - "message_id" - ], + "columns": ["message_id"], "tableTo": "message", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -625,13 +615,9 @@ "table": "part" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "tableTo": "project", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -640,13 +626,9 @@ "table": "permission" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "tableTo": "project", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -655,13 +637,9 @@ "table": "session" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -670,13 +648,9 @@ "table": "todo" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -685,64 +659,49 @@ "table": "session_share" }, { - "columns": [ - "session_id", - "position" - ], + "columns": ["session_id", "position"], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", @@ -834,4 +793,4 @@ } ], "renames": [] -} \ No newline at end of file +} diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index dc2e51e5fb..a67a56e30e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7214,12 +7214,40 @@ "properties": { "part": { "$ref": "#/components/schemas/Part" + } + }, + "required": ["part"] + } + }, + "required": ["type", "properties"] + }, + "Event.message.part.delta": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "message.part.delta" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "partID": { + "type": "string" + }, + "field": { + "type": "string" }, "delta": { "type": "string" } }, - "required": ["part"] + "required": ["sessionID", "messageID", "partID", "field", "delta"] } }, "required": ["type", "properties"] @@ -7633,13 +7661,9 @@ "priority": { "description": "Priority level of the task: high, medium, low", "type": "string" - }, - "id": { - "description": "Unique identifier for the todo item", - "type": "string" } }, - "required": ["content", "status", "priority", "id"] + "required": ["content", "status", "priority"] }, "Event.todo.updated": { "type": "object", @@ -8304,6 +8328,9 @@ { "$ref": "#/components/schemas/Event.message.part.updated" }, + { + "$ref": "#/components/schemas/Event.message.part.delta" + }, { "$ref": "#/components/schemas/Event.message.part.removed" }, From 7b72cc3a4839b0fc332ff496c3b1101d43337a6c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 30 Jan 2026 16:21:25 +0000 Subject: [PATCH 26/35] 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 f50c7a23df..4c9c1f2973 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-gUWzUsk81miIrjg0fZQmsIQG4pZYmEHgzN6BaXI+lfc=", - "aarch64-linux": "sha256-gwEG75ha/ojTO2iAObTmLTtEkXIXJ7BThzfI5CqlJh8=", - "aarch64-darwin": "sha256-20RGG2GkUItCzD67gDdoSLfexttM8abS//FKO9bfjoM=", - "x86_64-darwin": "sha256-i2VawFuR1UbjPVYoybU6aJDJfFo0tcvtl1aM31Y2mTQ=" + "x86_64-linux": "sha256-U1iENWBawuVAzT2adU7oder9RFX9NwXaWjmFA2wDH78=", + "aarch64-linux": "sha256-04rc4s6QA/YI2+gwUBvxbWJ7qljzGwBgc7sdIEZxGK4=", + "aarch64-darwin": "sha256-csypbReltWT10WxQkQBK8yauM/slXe0wruxKrUguz6A=", + "x86_64-darwin": "sha256-E1T/GR3HgZMm10nj/zEiPslapJh/+k6HJN+remO3new=" } } From 200aef2eb330296e5c9d2a0008fc40f7e34ce68a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 31 Jan 2026 16:28:07 -0500 Subject: [PATCH 27/35] sync --- .../test/acp/event-subscription.test.ts | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index 8e139ff597..1145a1357d 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -122,12 +122,20 @@ function createFakeAgent() { messages: async () => { return { data: [] } }, - message: async () => { + message: async (params?: any) => { + // Return a message with parts that can be looked up by partID return { data: { info: { role: "assistant", }, + parts: [ + { + id: params?.messageID ? `${params.messageID}_part` : "part_1", + type: "text", + text: "", + }, + ], }, } }, @@ -193,7 +201,7 @@ function createFakeAgent() { } describe("acp.agent event subscription", () => { - test("routes message.part.updated by the event sessionID (no cross-session pollution)", async () => { + test("routes message.part.delta by the event sessionID (no cross-session pollution)", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, @@ -207,14 +215,12 @@ describe("acp.agent event subscription", () => { controller.push({ directory: cwd, payload: { - type: "message.part.updated", + type: "message.part.delta", properties: { - part: { - sessionID: sessionB, - messageID: "msg_1", - type: "text", - synthetic: false, - }, + sessionID: sessionB, + messageID: "msg_1", + partID: "msg_1_part", + field: "text", delta: "hello", }, }, @@ -230,7 +236,7 @@ describe("acp.agent event subscription", () => { }) }) - test("keeps concurrent sessions isolated when message.part.updated events are interleaved", async () => { + test("keeps concurrent sessions isolated when message.part.delta events are interleaved", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, @@ -248,14 +254,12 @@ describe("acp.agent event subscription", () => { controller.push({ directory: cwd, payload: { - type: "message.part.updated", + type: "message.part.delta", properties: { - part: { - sessionID: sessionId, - messageID, - type: "text", - synthetic: false, - }, + sessionID: sessionId, + messageID, + partID: `${messageID}_part`, + field: "text", delta, }, }, @@ -402,14 +406,12 @@ describe("acp.agent event subscription", () => { controller.push({ directory: cwd, payload: { - type: "message.part.updated", + type: "message.part.delta", properties: { - part: { - sessionID: sessionB, - messageID: "msg_b", - type: "text", - synthetic: false, - }, + sessionID: sessionB, + messageID: "msg_b", + partID: "msg_b_part", + field: "text", delta: "session_b_message", }, }, From 61084e7f6f6f8cfec4d2904aac6854e2e3137786 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 31 Jan 2026 16:32:37 -0500 Subject: [PATCH 28/35] sync --- packages/opencode/src/storage/json-migration.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 179d5a064a..1901f1aad6 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -13,6 +13,20 @@ export namespace JsonMigration { export async function run(sqlite: Database) { const storageDir = path.join(Global.Path.data, "storage") + if (!(await Bun.file(storageDir).exists())) { + log.info("storage directory does not exist, skipping migration") + return { + projects: 0, + sessions: 0, + messages: 0, + parts: 0, + todos: 0, + permissions: 0, + shares: 0, + errors: [] as string[], + } + } + log.info("starting json to sqlite migration", { storageDir }) const start = performance.now() From 794ecab028890e7bbe6b46d8e1219d3b09ced92e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 1 Feb 2026 01:20:05 +0000 Subject: [PATCH 29/35] 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 b48e4cb0da..8341023e4d 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-3wRTDLo5FZoUc2Bwm1aAJZ4dNsekX8XoY6TwTmohgYo=", - "aarch64-linux": "sha256-CKiuc6c52UV9cLEtccYEYS4QN0jYzNJv1fHSayqbHKo=", - "aarch64-darwin": "sha256-jGr2udrVeseioMWpIzpjYFfS1CN8GvNFwo6o92Aa5Oc=", - "x86_64-darwin": "sha256-k5384Uun7tLjKkfJXXPcaZSXQ5jf/tMv21xi5cJU1rM=" + "x86_64-linux": "sha256-LGI4XJj9WhBwnnqCXVNOTygrB0rBFIIcMjMm1ZuqIQI=", + "aarch64-linux": "sha256-0L89lS1RcFmiz9qBRHftdtAZVOtoTG6X0RgEpaLI1sQ=", + "aarch64-darwin": "sha256-QdwEcYDtgo/5HIK5WPpV8cf/aZrH9ref/Fh2vS3m/CU=", + "x86_64-darwin": "sha256-YLMPQzo0hnSo722WbC+Cp88Db6oyQ+o9NQM8z/7t4uw=" } } From ed87341c4f60caec6b541aef5b7d613a6ee6f2d9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 31 Jan 2026 20:30:34 -0500 Subject: [PATCH 30/35] core: fix storage directory existence check during json migration Use fs.existsSync() instead of Bun.file().exists() since we're checking for a directory, not a file. --- packages/opencode/src/storage/json-migration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 1901f1aad6..a63774ee08 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -6,6 +6,7 @@ import { ProjectTable } from "../project/project.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" import { SessionShareTable } from "../share/share.sql" import path from "path" +import { existsSync } from "fs" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -13,7 +14,7 @@ export namespace JsonMigration { export async function run(sqlite: Database) { const storageDir = path.join(Global.Path.data, "storage") - if (!(await Bun.file(storageDir).exists())) { + if (!existsSync(storageDir)) { log.info("storage directory does not exist, skipping migration") return { projects: 0, From ad5b790bb33db597dc3b36bc378c8f6bc1522c6c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 31 Jan 2026 20:31:33 -0500 Subject: [PATCH 31/35] docs: simplify commit command by removing unnecessary instructions --- .opencode/command/commit.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index d8a420b173..e88932a244 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -16,15 +16,12 @@ wip: For anything in the packages/web use the docs: prefix. -For anything in the packages/app use the ignore: prefix. - prefer to explain WHY something was done from an end user perspective instead of WHAT was done. do not do generic messages like "improved agent experience" be very specific about what user facing changes were made -if there are changes do a git pull --rebase if there are conflicts DO NOT FIX THEM. notify me and I will fix them ## GIT DIFF From d4f053042c8d9301384d14e439682ad56a84fb3e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:26:58 -0500 Subject: [PATCH 32/35] sync --- packages/sdk/js/openapi.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/sdk/js/openapi.json diff --git a/packages/sdk/js/openapi.json b/packages/sdk/js/openapi.json new file mode 100644 index 0000000000..e69de29bb2 From b7a323355c08b49ac2d73c363b5dcd894e41ba76 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:27:29 -0500 Subject: [PATCH 33/35] fix: ExtractTablesWithRelations type parameter --- packages/console/core/src/drizzle/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/core/src/drizzle/index.ts b/packages/console/core/src/drizzle/index.ts index 99e8612f87..a9137b1f1f 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, Record> + ExtractTablesWithRelations> > const client = memo(() => { From 4ec629305461ec6cf15ded37fd591da6bf7e9f16 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:31:02 -0500 Subject: [PATCH 34/35] fix: type errors in console-core and session - Fix ExtractTablesWithRelations type compatibility with drizzle-orm beta - Migrate Session.list() and Session.children() from Storage to SQLite --- packages/console/core/src/drizzle/index.ts | 3 +-- packages/opencode/src/session/index.ts | 27 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/console/core/src/drizzle/index.ts b/packages/console/core/src/drizzle/index.ts index a9137b1f1f..d3a4b63bf3 100644 --- a/packages/console/core/src/drizzle/index.ts +++ b/packages/console/core/src/drizzle/index.ts @@ -4,7 +4,6 @@ export * from "drizzle-orm" import { Client } from "@planetscale/database" import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core" -import type { ExtractTablesWithRelations } from "drizzle-orm" import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless" import { Context } from "../context" import { memo } from "../util/memo" @@ -14,7 +13,7 @@ export namespace Database { PlanetscaleQueryResultHKT, PlanetScalePreparedQueryHKT, Record, - ExtractTablesWithRelations> + any > const client = memo(() => { diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 34b0e57b3a..1a8831d1dd 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,7 +10,7 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { Database, NotFoundError, eq } from "../storage/db" +import { Database, NotFoundError, eq, and } from "../storage/db" import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Storage } from "@/storage/storage" import { Log } from "../util/log" @@ -505,23 +505,24 @@ export namespace Session { export function* list() { const project = Instance.project - for (const item of await Storage.list(["session", project.id])) { - const session = await Storage.read(item).catch(() => undefined) - if (!session) continue - yield session + const rows = Database.use((db) => + db.select().from(SessionTable).where(eq(SessionTable.project_id, 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).catch(() => undefined) - if (!session) continue - if (session.parentID !== parentID) continue - result.push(session) - } - return result + const rows = Database.use((db) => + db + .select() + .from(SessionTable) + .where(and(eq(SessionTable.project_id, project.id), eq(SessionTable.parent_id, parentID))) + .all(), + ) + return rows.map(fromRow) }) export const remove = fn(Identifier.schema("session"), async (sessionID) => { From cb721497c129caf4d2f8ec4701a4177940618362 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 01:40:44 +0000 Subject: [PATCH 35/35] 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 6fe8f61d3d..fd8112e8dd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-06Otz3loT4vn0578VDxUqVudtzQvV7oM3EIzjZnsejo=", - "aarch64-linux": "sha256-88Qai5RkSenCZkakOg52b6xU2ok+h/Ns4/5L3+55sFY=", - "aarch64-darwin": "sha256-x8dgCF0CJBWi2dZLDHMGdlTqys1X755ok0PM6x0HAGo=", - "x86_64-darwin": "sha256-FkLDqorfIfOw+tB7SW5vgyhOIoI0IV9lqPW1iEmvUiI=" + "x86_64-linux": "sha256-PrEuJ7fh/rmd8ewXsnKgQ/Zu8qYsrb3D7maL9ZlVAnE=", + "aarch64-linux": "sha256-7+ktHTXtHMWWA9tNxX8Fb1um1JFKHQuIjIJPuveqL94=", + "aarch64-darwin": "sha256-KirMU9LO7sB0ufVaWF9Y+DtxbBVyapE02GP2ytW9xLg=", + "x86_64-darwin": "sha256-wZJt3htmjWvwRCIoD0rkr3+8cW/xjfXfz8rdHxcFplo=" } }