From ba499fb4059f8bb017074eb65e9f493eff591f61 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 13 Apr 2026 16:54:11 -0400 Subject: [PATCH] wip --- .opencode/opencode.jsonc | 1 + WORKSPACE_SESSION_DELETE_NOTES.md | 121 ++++ bun.lock | 250 ++++++- daytonaWorkspaceBootstrap.sh | 43 ++ daytonaWorkspacePlugin.ts | 206 ++++++ debugWorkspacePlugin.ts | 23 + package.json | 1 + .../dialog-session-delete-failed.tsx | 101 +++ .../cmd/tui/component/dialog-session-list.tsx | 96 ++- .../tui/component/dialog-workspace-create.tsx | 135 +++- .../cli/cmd/tui/component/prompt/index.tsx | 3 + .../src/cli/cmd/tui/context/local.tsx | 12 +- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 42 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 153 +++- .../opencode/src/control-plane/workspace.ts | 272 ++++++- packages/opencode/src/plugin/loader.ts | 1 + .../src/server/instance/middleware.ts | 97 ++- packages/opencode/src/server/instance/sync.ts | 118 +++ .../opencode/src/server/instance/workspace.ts | 66 ++ packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/proxy.ts | 55 +- packages/opencode/src/sync/index.ts | 19 + .../test/cli/tui/workspace-restore.test.ts | 89 +++ .../test/server/workspace-restore.test.ts | 268 +++++++ packages/opencode/test/sync/index.test.ts | 70 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 151 +++- packages/sdk/js/src/v2/gen/types.gen.ts | 682 ++++++++++-------- sync-routes.ts | 189 +++++ 28 files changed, 2845 insertions(+), 421 deletions(-) create mode 100644 WORKSPACE_SESSION_DELETE_NOTES.md create mode 100644 daytonaWorkspaceBootstrap.sh create mode 100644 daytonaWorkspacePlugin.ts create mode 100644 debugWorkspacePlugin.ts create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx create mode 100644 packages/opencode/src/server/instance/sync.ts create mode 100644 packages/opencode/test/cli/tui/workspace-restore.test.ts create mode 100644 packages/opencode/test/server/workspace-restore.test.ts create mode 100644 sync-routes.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 8380f7f719..049293000c 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -11,6 +11,7 @@ }, }, "mcp": {}, + "plugin": ["../daytonaWorkspacePlugin.ts", "../debugWorkspacePlugin.ts"], "tools": { "github-triage": false, "github-pr-search": false, diff --git a/WORKSPACE_SESSION_DELETE_NOTES.md b/WORKSPACE_SESSION_DELETE_NOTES.md new file mode 100644 index 0000000000..aa146e62ba --- /dev/null +++ b/WORKSPACE_SESSION_DELETE_NOTES.md @@ -0,0 +1,121 @@ +# Workspace Session Delete Notes + +## Current state + +- Deleting a workspace-backed session now removes the session record correctly. +- The UI still goes stale in some cases because the delete is handled locally and the expected event propagation is incomplete. + +## Important behavior discovered + +### 1. `DELETE /session/:id` is currently being handled locally + +This is because workspace routing was changed so path handling happens earlier, and we now always let `DELETE /session/:id` through the local route path. + +That is a little weird semantically, because for workspace-backed sessions the delete action conceptually belongs to the remote workspace session too. + +Relevant code: + +- `packages/opencode/src/server/router.ts` +- special-case for `DELETE /session/:id` + +### 2. Local delete works, but UI does not fully update from events + +The delete operation works server-side, but because we do not have an active instance in this local-delete path, we are not publishing the same events the TUI expects for immediate sync/UI updates. + +That means: + +- delete succeeds +- persistence updates +- but event-driven UI refresh may not happen + +## Root issue + +We need event publication for local handling of workspace session deletes, but in this code path we may not have an instance context. + +So the current system is in an awkward middle state: + +- delete is handled locally +- remote session/workspace semantics still matter +- but local event publishing is instance-dependent in places + +## Key design question + +Where should the "delete last session in workspace -> delete workspace" logic live? + +### Option A: handle it remotely + +If the remote workspace handles session deletion and also decides whether the workspace should be deleted, then the result can sync back naturally. + +Pros: + +- cleaner ownership model +- remote workspace remains source of truth for workspace-backed session lifecycle +- sync/event flow stays more consistent + +Cons: + +- requires remote delete path to be used reliably +- local special-casing in router becomes more suspect + +### Option B: handle it locally + +If local server deletes the session and then checks whether any sessions remain for that `workspaceID`, local can also delete the workspace. + +Pros: + +- straightforward to implement +- does not depend on remote behavior + +Cons: + +- local path now owns workspace lifecycle decisions for remote workspaces +- still has event propagation problems unless we explicitly publish/update correctly + +## Current leaning + +The workspace cleanup logic probably belongs on the remote side if workspace-backed sessions are supposed to behave as remote-owned state. + +Reason: + +- if remote handles it, the result can sync back +- avoids local special-case ownership drift + +But this depends on whether `DELETE /session/:id` should actually be routed remotely for workspace sessions instead of always being forced local. + +## Things to inspect next + +1. `packages/opencode/src/server/router.ts` + +- Revisit why `DELETE /session/:id` is forced local. +- Decide whether workspace-backed session deletes should proxy to remote instead. + +2. `packages/opencode/src/session/index.ts` + +- Current local cleanup logic removes workspace if no sessions remain for its `workspaceID`. +- Re-evaluate whether this should stay here or move to remote handling. + +3. Event publication path + +- Figure out what event(s) the TUI actually needs to update correctly after delete. +- Check whether local delete without instance can still publish enough global/sync events. + +4. TUI refresh path + +- `packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx` +- `packages/opencode/src/cli/cmd/tui/context/sync.tsx` + +We added an explicit session-list refresh to work around stale UI, but that is treating the symptom. + +## Summary + +The real unresolved issue is ownership: + +- local currently handles `DELETE /session/:id` +- remote workspace semantics still matter +- event propagation is incomplete when delete is handled without an instance + +Next session should start by deciding: + +1. Should workspace session delete be handled locally or remotely? +2. Where should "delete workspace if no sessions remain" live? +3. What event must be published so the UI updates without manual refresh? diff --git a/bun.lock b/bun.lock index 7ff8a3072f..7c6eaec9ea 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "opencode", "dependencies": { "@aws-sdk/client-s3": "3.933.0", + "@daytona/sdk": "0.164.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -628,6 +629,7 @@ "trustedDependencies": [ "esbuild", "tree-sitter-powershell", + "protobufjs", "electron", "web-tree-sitter", "tree-sitter-bash", @@ -836,6 +838,8 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1028.0", "", { "dependencies": { "@smithy/middleware-endpoint": "^4.4.29", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1028.0" } }, "sha512-AT937nfpMDW/8oDiWPBP/BdGJ6943ALMWTBpUi0fD0qelA3lyZgErSnX7yp9j3t/enzyHdlyBOPq9kGFBt0Xcg=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g=="], @@ -1020,6 +1024,12 @@ "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + "@daytona/api-client": ["@daytona/api-client@0.164.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-G1cV6gC0mTOMSkGy4C5FFkxEytNItOzNCWXtnYVGepLqcLsnXuv1+vpIJFiIsfKEjrZKax8bu2sdSA8lQHTyOw=="], + + "@daytona/sdk": ["@daytona/sdk@0.164.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/api-client": "0.164.0", "@daytona/toolbox-api-client": "0.164.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", "@opentelemetry/instrumentation-http": "^0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "axios": "^1.13.5", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-EBxKyWjvbMMR7ZpcHp+QvtJHPeCQ7SVO08okEWMLiKeJ0bqSyXgzYW5gbXUyhhogOkcDptD+j89yK1EnTTWjSg=="], + + "@daytona/toolbox-api-client": ["@daytona/toolbox-api-client@0.164.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-7LU6mxPuobkgDIg24MHP7kf36Wm1kSSvorM/8ODGTkbLfUdXk7DSTGBs9Lqoy0cM4/62LJ9kPmjw607pPSTXbQ=="], + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], @@ -1168,6 +1178,10 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], @@ -1186,6 +1200,8 @@ "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="], + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], @@ -1316,6 +1332,8 @@ "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@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=="], @@ -1544,6 +1562,62 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentui/core": ["@opentui/core@0.1.97", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.97", "@opentui/core-darwin-x64": "0.1.97", "@opentui/core-linux-arm64": "0.1.97", "@opentui/core-linux-x64": "0.1.97", "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-2ENH0Dc4NUAeHeeQCQhF1lg68RuyntOUP68UvortvDqTz/hqLG0tIwF+DboCKtWi8Nmao4SAQEJ7lfmyQNEDOQ=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="], @@ -1698,6 +1772,26 @@ "@protobuf-ts/runtime-rpc": ["@protobuf-ts/runtime-rpc@2.11.1", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], @@ -2378,6 +2472,8 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], @@ -2600,6 +2696,8 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], @@ -2660,6 +2758,8 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], @@ -2870,7 +2970,7 @@ "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], @@ -3022,6 +3122,8 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], @@ -3122,6 +3224,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], @@ -3278,6 +3382,8 @@ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], @@ -3330,6 +3436,8 @@ "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -3592,6 +3700,8 @@ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], @@ -3822,6 +3932,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4024,6 +4136,8 @@ "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -4166,6 +4280,8 @@ "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], @@ -4302,6 +4418,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], @@ -4412,6 +4530,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="], "shikiji": ["shikiji@0.6.13", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="], @@ -4540,8 +4660,12 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.11", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.4", "@storybook/builder-vite": "^10.3.1", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.11" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-aDe2ipD0HAH54zsQbbCUI9AcBPiLwljtdx+GRmyHsHaPyij1KmGVKo5N004qXZfQaoK0uru9DsdnWon5rRkjfg=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -4956,7 +5080,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -5192,6 +5316,16 @@ "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.29", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-serde": "^4.2.17", "@smithy/node-config-provider": "^4.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw=="], + + "@aws-sdk/lib-storage/@smithy/protocol-http": ["@smithy/protocol-http@5.3.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client": ["@smithy/smithy-client@4.12.9", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-stack": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ=="], + + "@aws-sdk/lib-storage/@smithy/types": ["@smithy/types@4.14.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ=="], + + "@aws-sdk/lib-storage/buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="], "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], @@ -5290,6 +5424,8 @@ "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5442,6 +5578,26 @@ "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], @@ -5572,6 +5728,8 @@ "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "app-builder-lib/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -5616,8 +5774,6 @@ "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -5648,6 +5804,8 @@ "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -5878,6 +6036,8 @@ "storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="], + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -5886,8 +6046,6 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "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=="], @@ -6162,6 +6320,26 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.23.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.17", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.13", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.8", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.13", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.23.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.22", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="], + + "@aws-sdk/lib-storage/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="], "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], @@ -6242,6 +6420,10 @@ "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -6540,6 +6722,10 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], "motion/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], @@ -6556,6 +6742,8 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -6720,6 +6908,26 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.22", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser": ["@smithy/url-parser@4.2.13", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.16", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.2", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], @@ -6750,6 +6958,14 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -6834,6 +7050,8 @@ "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], @@ -6950,6 +7168,16 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.16", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.2", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], @@ -6968,6 +7196,10 @@ "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], @@ -7008,6 +7240,10 @@ "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="], + "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -7028,6 +7264,8 @@ "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], + "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/daytonaWorkspaceBootstrap.sh b/daytonaWorkspaceBootstrap.sh new file mode 100644 index 0000000000..492f066303 --- /dev/null +++ b/daytonaWorkspaceBootstrap.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euox pipefail + +project="$1" + +root="/home/daytona/workspace" +repo="$root/repo" +localbin="/home/daytona/opencode" +installbin="/home/daytona/.opencode/bin/opencode" + +printf "%s\n" "ipv4" > "$HOME/.curlrc" +rm -rf "$repo" +mkdir -p "$root" +tar -xzf "$HOME/repo.tgz" -C "$HOME/workspace" + +ls -last "$HOME" + +if [ -f "$HOME/opencode" ]; then + chmod +x "$HOME/opencode" + exe="$localbin" +else + mkdir -p "$HOME/.opencode/bin" + OPENCODE_INSTALL_DIR="$HOME/.opencode/bin" curl -4 -fsSL https://opencode.ai/install | bash + exe="$installbin" +fi + +echo "opencode: $exe" +printf "%s\n" "$project" > "$repo/.git/opencode" + +cd "$repo" +OPENCODE_WORKSPACE=true OPENCODE_EXPERIMENTAL_WORKSPACES=true nohup "$exe" serve --hostname 0.0.0.0 --port 3096 --print-logs > /tmp/opencode-server.log 2>&1 & + +for i in $(seq 1 60); do + if curl -4 -fsS http://127.0.0.1:3096/global/health >/dev/null; then + echo "ready" + exit 0 + fi + echo "waiting for server ($i/60)" + sleep 1 +done + +echo "daytona workspace server did not become ready in time" >&2 +exit 1 diff --git a/daytonaWorkspacePlugin.ts b/daytonaWorkspacePlugin.ts new file mode 100644 index 0000000000..db56ebbaa7 --- /dev/null +++ b/daytonaWorkspacePlugin.ts @@ -0,0 +1,206 @@ +import type { Daytona, Sandbox } from "@daytonaio/sdk" +import type { Plugin } from "@opencode-ai/plugin" +import { join } from "node:path" +import { fileURLToPath } from "node:url" +import { tmpdir } from "node:os" +import { access, mkdir } from "node:fs/promises" +import { randomUUID } from "node:crypto" + +let client: Promise | undefined + +let daytona = function daytona(): Promise { + if (client == null) { + client = import("@daytonaio/sdk").then( + ({ Daytona }) => + new Daytona({ + apiKey: "dtn_2ffe19d27837953f1a46cc297d8a5331d4c46b00856eb5f4a4afded3f3426038", + }), + ) + } + return client +} + +const preview = new Map() +const repo = "/home/daytona/workspace/repo" + +const local = fileURLToPath( + new URL("./packages/opencode/dist/opencode-linux-x64-baseline/bin/opencode", import.meta.url), +) +const bootstrap = fileURLToPath(new URL("./daytonaWorkspaceBootstrap.sh", import.meta.url)) + +async function exists(file: string) { + return access(file) + .then(() => true) + .catch(() => false) +} + +function sh(value: string) { + return `'${value.replace(/'/g, `'"'"'`)}'` +} + +async function boot() { + return Bun.file(bootstrap).text() +} + +// Internally Daytona uses axios, which tries to overwrite stack +// traces when a failure happens. That path fails in Bun, however, so +// when something goes wrong you only see a very obscure error. +async function withSandbox(name: string, fn: (sandbox: Sandbox) => Promise) { + const stack = Error.captureStackTrace + // @ts-expect-error temporary compatibility hack for Daytona's axios stack handling in Bun + Error.captureStackTrace = undefined + try { + return await fn(await (await daytona()).get(name)) + } finally { + Error.captureStackTrace = stack + } +} + +export const DaytonaWorkspacePlugin: Plugin = async ({ experimental_workspace, worktree, project }) => { + experimental_workspace.register("daytona", { + name: "Daytona", + description: "Create a remote Daytona workspace", + configure(config) { + return config + }, + async create(config) { + const temp = join(tmpdir(), `opencode-daytona-${randomUUID()}`) + + console.log("creating sandbox...") + + const sandbox = await ( + await daytona() + ).create({ + name: config.name, + envVars: { + foo: "bar", + }, + }) + + const sid = `setup-${randomUUID()}` + await sandbox.process.createSession(sid) + + try { + console.log("creating ssh...") + + const ssh = await withSandbox(config.name, (sandbox) => sandbox.createSshAccess()) + console.log("daytona:", ssh.sshCommand) + + const run = async (command: string, opts?: { stream?: boolean }) => { + if (!opts?.stream) { + const result = await sandbox.process.executeCommand(command) + if (result.exitCode === 0) return result + throw new Error(result.result || `sandbox command failed: ${command}`) + } + + const res = await sandbox.process.executeSessionCommand(sid, { command, runAsync: true }) + if (!res.cmdId) throw new Error(`sandbox command failed to start: ${command}`) + + let out = "" + let err = "" + await sandbox.process.getSessionCommandLogs( + sid, + res.cmdId, + (chunk) => { + out += chunk + process.stdout.write(chunk) + }, + (chunk) => { + err += chunk + process.stderr.write(chunk) + }, + ) + + for (let i = 0; i < 120; i++) { + const cmd = await sandbox.process.getSessionCommand(sid, res.cmdId) + if (typeof cmd.exitCode !== "number") { + await Bun.sleep(500) + continue + } + if (cmd.exitCode === 0) return cmd + throw new Error(err || out || `sandbox command failed: ${command}`) + } + + throw new Error(`sandbox command timed out waiting for exit code: ${command}`) + } + + const dir = join(temp, "repo") + const tar = join(temp, "repo.tgz") + const scr = join(temp, "bootstrap.sh") + const source = `file://${worktree}` + await mkdir(temp, { recursive: true }) + const args = ["clone", "--depth", "1", "--no-local"] + if (config.branch) args.push("--branch", config.branch) + args.push(source, dir) + + console.log("git cloning...") + + const clone = Bun.spawn(["git", ...args], { + cwd: tmpdir(), + stdout: "pipe", + stderr: "pipe", + }) + const code = await clone.exited + if (code !== 0) throw new Error(await new Response(clone.stderr).text()) + + console.log("tarring...") + + const packed = Bun.spawn(["tar", "-czf", tar, "-C", temp, "repo"], { + stdout: "ignore", + stderr: "pipe", + }) + if ((await packed.exited) !== 0) throw new Error(await new Response(packed.stderr).text()) + + console.log("writing bootstrap script...") + + await Bun.write(scr, await boot()) + + console.log("uploading files...") + + await sandbox.fs.uploadFile(tar, "repo.tgz") + await sandbox.fs.uploadFile(scr, "bootstrap.sh") + + console.log("local", local) + if (await exists(local)) { + console.log("uploading local binary...") + await sandbox.fs.uploadFile(local, "opencode") + } + + console.log("bootstrapping workspace...") + + await run(`bash bootstrap.sh ${sh(project.id)}`, { + stream: true, + }) + return + } finally { + await sandbox.process.deleteSession(sid).catch(() => undefined) + } + }, + async remove(config) { + const sandbox = await (await daytona()).get(config.name).catch(() => undefined) + if (!sandbox) return + await (await daytona()).delete(sandbox) + preview.delete(config.name) + }, + async target(config) { + let link = preview.get(config.name) + if (!link) { + link = await withSandbox(config.name, (sandbox) => sandbox.getPreviewLink(3096)) + preview.set(config.name, link) + } + return { + type: "remote", + url: link.url, + headers: { + "x-daytona-preview-token": link.token, + "x-daytona-skip-preview-warning": "true", + "x-opencode-directory": repo, + }, + } + }, + }) + + return {} +} + +export default DaytonaWorkspacePlugin diff --git a/debugWorkspacePlugin.ts b/debugWorkspacePlugin.ts new file mode 100644 index 0000000000..bfb0ee8a2f --- /dev/null +++ b/debugWorkspacePlugin.ts @@ -0,0 +1,23 @@ +import type { Plugin } from "@opencode-ai/plugin" + +export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => { + experimental_workspace.register("debug", { + name: "Debug", + description: "Create a debugging server", + configure(config) { + return config + }, + async create(_config) {}, + async remove(_config) {}, + target(_config) { + return { + type: "remote", + url: "http://localhost:5096/", + } + }, + }) + + return {} +} + +export default DebugWorkspacePlugin diff --git a/package.json b/package.json index d08bada052..12d72ed56a 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "3.933.0", + "@daytona/sdk": "0.164.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx new file mode 100644 index 0000000000..4a22a0c492 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx @@ -0,0 +1,101 @@ +import { TextAttributes } from "@opentui/core" +import { useTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" +import { createStore } from "solid-js/store" +import { For } from "solid-js" +import { useKeyboard } from "@opentui/solid" + +export function DialogSessionDeleteFailed(props: { + session: string + workspace: string + onDelete?: () => boolean | void | Promise + onRestore?: () => boolean | void | Promise + onDone?: () => void +}) { + const dialog = useDialog() + const { theme } = useTheme() + const [store, setStore] = createStore({ + active: "delete" as "delete" | "restore", + }) + + const options = [ + { + id: "delete" as const, + title: "Delete workspace", + description: "Delete the workspace and all sessions attached to it.", + run: props.onDelete, + }, + { + id: "restore" as const, + title: "Restore to new workspace", + description: "Try to restore this session into a new workspace.", + run: props.onRestore, + }, + ] + + async function confirm() { + const result = await options.find((item) => item.id === store.active)?.run?.() + if (result === false) return + props.onDone?.() + if (!props.onDone) dialog.clear() + } + + useKeyboard((evt) => { + if (evt.name === "return") { + void confirm() + } + if (evt.name === "left" || evt.name === "up") { + setStore("active", "delete") + } + if (evt.name === "right" || evt.name === "down") { + setStore("active", "restore") + } + }) + + return ( + + + + Failed to Delete Session + + dialog.clear()}> + esc + + + + {`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`} + + + Choose how you want to recover this broken workspace session. + + + + {(item) => ( + { + setStore("active", item.id) + void confirm() + }} + > + + {item.title} + + + {item.description} + + + )} + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 9ecb21e82a..da681106ea 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -13,8 +13,10 @@ import { DialogSessionRename } from "./dialog-session-rename" import { Keybind } from "@/util/keybind" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" -import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create" +import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create" import { Spinner } from "./spinner" +import { errorMessage } from "@/util/error" +import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed" type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" @@ -30,7 +32,7 @@ export function DialogSessionList() { const [toDelete, setToDelete] = createSignal() const [search, setSearch] = createDebouncedSignal("", 150) - const [searchResults] = createResource(search, async (query) => { + const [searchResults, { refetch }] = createResource(search, async (query) => { if (!query) return undefined const result = await sdk.client.session.list({ search: query, limit: 30 }) return result.data ?? [] @@ -56,6 +58,57 @@ export function DialogSessionList() { )) } + function recover(session: NonNullable[number]>) { + const workspace = project.workspace.get(session.workspaceID!) + const list = () => dialog.replace(() => ) + dialog.replace(() => ( + { + const current = currentSessionID() + const info = current ? sync.data.session.find((item) => item.id === current) : undefined + const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! }) + if (result.error) { + toast.show({ + variant: "error", + title: "Failed to delete workspace", + message: errorMessage(result.error), + }) + return false + } + await project.workspace.sync() + await sync.session.refresh() + if (search()) await refetch() + if (info?.workspaceID === session.workspaceID) { + route.navigate({ type: "home" }) + } + return true + }} + onRestore={() => { + dialog.replace(() => ( + + restoreWorkspaceSession({ + dialog, + sdk, + sync, + project, + toast, + workspaceID, + sessionID: session.id, + done: list, + }) + } + /> + )) + return false + }} + /> + )) + } + const options = createMemo(() => { const today = new Date().toDateString() return sessions() @@ -145,9 +198,42 @@ export function DialogSessionList() { title: "delete", onTrigger: async (option) => { if (toDelete() === option.value) { - sdk.client.session.delete({ - sessionID: option.value, - }) + const session = sessions().find((item) => item.id === option.value) + const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined + try { + const result = await sdk.client.session.delete({ + sessionID: option.value, + }) + if (result.error) { + if (session?.workspaceID) { + recover(session) + } else { + toast.show({ + variant: "error", + title: "Failed to delete session", + message: errorMessage(result.error), + }) + } + setToDelete(undefined) + return + } + } catch (err) { + if (session?.workspaceID) { + recover(session) + } else { + toast.show({ + variant: "error", + title: "Failed to delete session", + message: errorMessage(err), + }) + } + setToDelete(undefined) + return + } + if (status && status !== "connected") { + await sync.session.refresh() + } + if (search()) await refetch() setToDelete(undefined) return } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 447a1c3258..f9caa88656 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -6,6 +6,8 @@ import { useSync } from "@tui/context/sync" import { useProject } from "@tui/context/project" import { createMemo, createSignal, onMount } from "solid-js" import { setTimeout as sleep } from "node:timers/promises" +import { errorData, errorMessage } from "@/util/error" +import { Log } from "@/util/log" import { useSDK } from "../context/sdk" import { useToast } from "../ui/toast" @@ -15,6 +17,8 @@ type Adaptor = { description: string } +const log = Log.Default.clone().tag("service", "tui-workspace") + function scoped(sdk: ReturnType, sync: ReturnType, workspaceID: string) { return createOpencodeClient({ baseUrl: sdk.url, @@ -33,8 +37,19 @@ export async function openWorkspaceSession(input: { workspaceID: string }) { const client = scoped(input.sdk, input.sync, input.workspaceID) + log.info("workspace session create requested", { + workspaceID: input.workspaceID, + }) while (true) { - const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined) + const result = await client.session + .create({ workspaceID: input.workspaceID, workspace: input.workspaceID }) + .catch((err) => { + log.error("workspace session create request failed", { + workspaceID: input.workspaceID, + error: errorData(err), + }) + return undefined + }) if (!result) { input.toast.show({ message: "Failed to create workspace session", @@ -42,26 +57,115 @@ export async function openWorkspaceSession(input: { }) return } - if (result.response.status >= 500 && result.response.status < 600) { + log.info("workspace session create response", { + workspaceID: input.workspaceID, + status: result.response?.status, + sessionID: result.data?.id, + }) + if (result.response?.status && result.response.status >= 500 && result.response.status < 600) { + log.warn("workspace session create retrying after server error", { + workspaceID: input.workspaceID, + status: result.response.status, + }) await sleep(1000) continue } if (!result.data) { + log.error("workspace session create returned no data", { + workspaceID: input.workspaceID, + status: result.response?.status, + }) input.toast.show({ message: "Failed to create workspace session", variant: "error", }) return } + + await sleep(5000) + input.route.navigate({ type: "session", sessionID: result.data.id, }) + log.info("workspace session create complete", { + workspaceID: input.workspaceID, + sessionID: result.data.id, + }) input.dialog.clear() return } } +export async function restoreWorkspaceSession(input: { + dialog: ReturnType + sdk: ReturnType + sync: ReturnType + project: ReturnType + toast: ReturnType + workspaceID: string + sessionID: string + done?: () => void +}) { + log.info("session restore requested", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + }) + const result = await input.sdk.client.experimental.workspace + .sessionRestore({ id: input.workspaceID, sessionID: input.sessionID }) + .catch((err) => { + log.error("session restore request failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + return undefined + }) + if (!result?.data) { + log.error("session restore failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + status: result?.response?.status, + error: result?.error ? errorData(result.error) : undefined, + }) + input.toast.show({ + message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`, + variant: "error", + }) + return + } + + log.info("session restore response", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + status: result.response?.status, + total: result.data.total, + }) + + await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => { + log.error("session restore refresh failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + throw err + }) + + log.info("session restore complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total: result.data.total, + }) + + input.toast.show({ + message: "Session restored into the new workspace", + variant: "success", + }) + input.done?.() + if (input.done) return + input.dialog.clear() +} + export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise | void }) { const dialog = useDialog() const sync = useSync() @@ -123,18 +227,41 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = const create = async (type: string) => { if (creating()) return setCreating(type) + log.info("workspace create requested", { + type, + }) - const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined) + const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => { + log.error("workspace create request failed", { + type, + error: errorData(err), + }) + return undefined + }) const workspace = result?.data if (!workspace) { setCreating(undefined) + log.error("workspace create failed", { + type, + status: result?.response.status, + error: result?.error ? errorData(result.error) : undefined, + }) toast.show({ - message: "Failed to create workspace", + message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, variant: "error", }) return } + log.info("workspace create response", { + type, + workspaceID: workspace.id, + status: result.response?.status, + }) await project.workspace.sync() + log.info("workspace create synced", { + type, + workspaceID: workspace.id, + }) await props.onSelect(workspace.id) setCreating(undefined) } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 5a3e1d451d..9ad246b249 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -611,8 +611,11 @@ export function Prompt(props: PromptProps) { } let sessionID = props.sessionID + console.log("jwl", sessionID) if (sessionID == null) { + console.log("creating") const res = await sdk.client.session.create({ + workspace: props.workspaceID, workspaceID: props.workspaceID, }) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index ec3931b209..c752d9ac1b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -21,8 +21,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const sdk = useSDK() const toast = useToast() + function providers() { + return sync.data?.provider ?? [] + } + function isModelValid(model: { providerID: string; modelID: string }) { - const provider = sync.data.provider.find((x) => x.id === model.providerID) + const provider = providers().find((x) => x.id === model.providerID) return !!provider?.models[model.modelID] } @@ -178,7 +182,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - const provider = sync.data.provider[0] + const provider = providers()[0] if (!provider) return undefined const defaultModel = sync.data.provider_default[provider.id] const firstModel = Object.values(provider.models)[0] @@ -221,7 +225,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ reasoning: false, } } - const provider = sync.data.provider.find((x) => x.id === value.providerID) + const provider = providers().find((x) => x.id === value.providerID) const info = provider?.models[value.modelID] return { provider: provider?.name ?? value.providerID, @@ -336,7 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ list() { const m = currentModel() if (!m) return [] - const provider = sync.data.provider.find((x) => x.id === m.providerID) + const provider = providers().find((x) => x.id === m.providerID) const info = provider?.models[m.modelID] if (!info?.variants) return [] return Object.keys(info.variants) diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index ad35aa45c2..23dcbabd70 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -3,6 +3,8 @@ import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup, onMount } from "solid-js" +import { Log } from "@/util/log" +import { errorData } from "@/util/error" export type EventSource = { subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void> @@ -19,13 +21,49 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }) => { const abort = new AbortController() let sse: AbortController | undefined + const log = Log.Default.clone().tag("service", "tui-sdk") + + const raw = props.fetch ?? fetch + + const traced: typeof fetch = async (input, init) => { + const req = input instanceof Request ? input : new Request(input, init) + const start = Date.now() + try { + const res = await raw(req) + const url = new URL(res.url || req.url) + if (!res.ok || url.searchParams.get("workspace")) { + const body = await res + .clone() + .text() + .catch(() => "") + log.info("sdk fetch", { + method: req.method, + request: req.url, + response: res.url || req.url, + status: res.status, + duration: Date.now() - start, + workspace: url.searchParams.get("workspace"), + body: body.slice(0, 1000), + }) + } + return res + } catch (error) { + log.error("sdk fetch failed", { + method: req.method, + request: req.url, + duration: Date.now() - start, + error: errorData(error), + }) + throw error + } + } function createSDK() { return createOpencodeClient({ baseUrl: props.url, signal: abort.signal, directory: props.directory, - fetch: props.fetch, + fetch: traced, headers: props.headers, }) } @@ -109,7 +147,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }, directory: props.directory, event: emitter, - fetch: props.fetch ?? fetch, + fetch: traced, url: props.url, } }, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 498db99a1b..1e559ba7c0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -29,6 +29,7 @@ import { useExit } from "./exit" import { useArgs } from "./args" import { batch, createEffect, on } from "solid-js" import { Log } from "@/util/log" +import { errorData } from "@/util/error" import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state" export const { use: useSync, provider: SyncProvider } = createSimpleContext({ @@ -107,11 +108,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const event = useEvent() const project = useProject() const sdk = useSDK() + const log = Log.Default.clone().tag("service", "tui-sync") event.subscribe((event) => { switch (event.type) { case "server.instance.disposed": - bootstrap() + log.info("bootstrap triggered by instance disposal", { + workspace: project.workspace.current(), + }) + void bootstrap().catch((error) => { + log.error("bootstrap after disposal failed", { + workspace: project.workspace.current(), + error: errorData(error), + }) + }) break case "permission.replied": { const requests = store.permission[event.properties.sessionID] @@ -351,30 +361,63 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const args = useArgs() async function bootstrap() { - console.log("bootstrapping") const workspace = project.workspace.current() const start = Date.now() - 30 * 24 * 60 * 60 * 1000 + log.info("bootstrap started", { workspace, path: project.instance.path() }) + + function track(name: string, promise: Promise) { + return promise + .then((value) => { + log.debug("bootstrap request ok", { name, workspace }) + return value + }) + .catch((error) => { + log.error("bootstrap request failed", { + name, + workspace, + error: errorData(error), + }) + throw error + }) + } + const sessionListPromise = sdk.client.session .list({ start: start }) .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) // blocking - include session.list when continuing a session - const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true }) - const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true }) - const consoleStatePromise = sdk.client.experimental.console - .get({ workspace }, { throwOnError: true }) - .then((x) => ConsoleState.parse(x.data)) - .catch(() => emptyConsoleState) - const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true }) - const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true }) - const projectPromise = project.sync() + const providersPromise = track( + "config.providers", + sdk.client.config.providers({ workspace }, { throwOnError: true }), + ) + const providerListPromise = track( + "provider.list", + sdk.client.provider.list({ workspace }, { throwOnError: true }), + ) + const consoleStatePromise = track( + "experimental.console.get", + sdk.client.experimental.console + .get({ workspace }, { throwOnError: true }) + .then((x) => ConsoleState.parse(x.data)) + .catch((error) => { + log.warn("console state unavailable", { + workspace, + error: errorData(error), + }) + return emptyConsoleState + }), + ) + const agentsPromise = track("app.agents", sdk.client.app.agents({ workspace }, { throwOnError: true })) + const configPromise = track("config.get", sdk.client.config.get({ workspace }, { throwOnError: true })) + const projectPromise = track("project.sync", project.sync()) + const sessionSyncPromise = track("session.list", sessionListPromise) const blockingRequests: Promise[] = [ providersPromise, providerListPromise, agentsPromise, configPromise, projectPromise, - ...(args.continue ? [sessionListPromise] : []), + ...(args.continue ? [sessionSyncPromise] : []), ] await Promise.all(blockingRequests) @@ -384,7 +427,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const consoleStateResponse = consoleStatePromise const agentsResponse = agentsPromise.then((x) => x.data ?? []) const configResponse = configPromise.then((x) => x.data!) - const sessionListResponse = args.continue ? sessionListPromise : undefined + const sessionListResponse = args.continue ? sessionSyncPromise : undefined return Promise.all([ providersResponse, @@ -414,34 +457,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) .then(() => { if (store.status !== "complete") setStore("status", "partial") + log.info("bootstrap partial", { workspace }) // non-blocking Promise.all([ - ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), - consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))), - sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))), - sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data!))), - sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data!))), - sdk.client.experimental.resource - .list({ workspace }) - .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), - sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))), - sdk.client.session.status({ workspace }).then((x) => { + ...(args.continue + ? [] + : [ + track("session.list.background", sessionListPromise).then((sessions) => + setStore("session", reconcile(sessions)), + ), + ]), + track("experimental.console.get.background", consoleStatePromise).then((consoleState) => + setStore("console_state", reconcile(consoleState)), + ), + track("command.list", sdk.client.command.list({ workspace })).then((x) => + setStore("command", reconcile(x.data ?? [])), + ), + track("lsp.status", sdk.client.lsp.status({ workspace })).then((x) => setStore("lsp", reconcile(x.data!))), + track("mcp.status", sdk.client.mcp.status({ workspace })).then((x) => setStore("mcp", reconcile(x.data!))), + track("experimental.resource.list", sdk.client.experimental.resource.list({ workspace })).then((x) => + setStore("mcp_resource", reconcile(x.data ?? {})), + ), + track("formatter.status", sdk.client.formatter.status({ workspace })).then((x) => + setStore("formatter", reconcile(x.data!)), + ), + track("session.status", sdk.client.session.status({ workspace })).then((x) => { setStore("session_status", reconcile(x.data!)) }), - sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))), - sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))), - project.workspace.sync(), - ]).then(() => { - setStore("status", "complete") - }) + track("provider.auth", sdk.client.provider.auth({ workspace })).then((x) => + setStore("provider_auth", reconcile(x.data ?? {})), + ), + track("vcs.get", sdk.client.vcs.get({ workspace })).then((x) => setStore("vcs", reconcile(x.data))), + track("project.workspace.sync", project.workspace.sync()), + ]) + .then(() => { + log.info("bootstrap complete", { workspace }) + setStore("status", "complete") + }) + .catch((error) => { + log.error("bootstrap background failed", { + workspace, + error: errorData(error), + }) + throw error + }) }) - .catch(async (e) => { - Log.Default.error("tui bootstrap failed", { - error: e instanceof Error ? e.message : String(e), - name: e instanceof Error ? e.name : undefined, - stack: e instanceof Error ? e.stack : undefined, + .catch((error) => { + log.error("bootstrap failed", { + workspace, + error: errorData(error), }) - await exit(e) + throw error }) } @@ -451,7 +517,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ () => project.workspace.current(), () => { fullSyncedSessions.clear() - void bootstrap() + log.info("bootstrap triggered by workspace change", { + workspace: project.workspace.current(), + }) + void bootstrap().catch((error) => { + log.error("bootstrap effect failed", { + workspace: project.workspace.current(), + error: errorData(error), + }) + }) }, ), ) @@ -474,6 +548,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (match.found) return store.session[match.index] return undefined }, + async refresh() { + const start = Date.now() - 30 * 24 * 60 * 60 * 1000 + const sessions = await sdk.client.session + .list({ start }) + .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) + setStore("session", reconcile(sessions)) + }, status(sessionID: string) { const session = result.session.get(sessionID) if (!session) return "idle" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index f330e07b7a..e1104ed936 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,11 +1,13 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" -import { Database, eq } from "@/storage/db" +import { Database, asc, eq } from "@/storage/db" import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { SyncEvent } from "@/sync" +import { EventTable } from "@/sync/event.sql" +import { EventID } from "@/sync/schema" import { Log } from "@/util/log" import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" @@ -15,6 +17,10 @@ import { getAdaptor } from "./adaptors" import { WorkspaceInfo } from "./types" import { WorkspaceID } from "./schema" import { parseSSE } from "./sse" +import { Session } from "@/session" +import { SessionTable } from "@/session/session.sql" +import { SessionID } from "@/session/schema" +import { errorData } from "@/util/error" export namespace Workspace { export const Info = WorkspaceInfo.meta({ @@ -29,6 +35,13 @@ export namespace Workspace { }) export type ConnectionStatus = z.infer + const Restore = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + total: z.number().int().min(0), + step: z.number().int().min(0), + }) + export const Event = { Ready: BusEvent.define( "workspace.ready", @@ -42,6 +55,7 @@ export namespace Workspace { message: z.string(), }), ), + Restore: BusEvent.define("workspace.restore", Restore), Status: BusEvent.define("workspace.status", ConnectionStatus), } @@ -97,16 +111,186 @@ export namespace Workspace { await adaptor.create(config) + console.log("starting sync") startSync(info) return info }) + const SessionRestoreInput = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + }) + + export const sessionRestore = fn(SessionRestoreInput, async (input) => { + log.info("session restore requested", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + }) + try { + const space = await get(input.workspaceID) + if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) + + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + const rows = Database.use((db) => + db + .select({ + id: EventTable.id, + aggregateID: EventTable.aggregate_id, + seq: EventTable.seq, + type: EventTable.type, + data: EventTable.data, + }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, input.sessionID)) + .orderBy(asc(EventTable.seq)) + .all(), + ) + if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`) + + const next = rows.at(-1)!.seq + 1 + const all = [ + ...rows, + { + id: EventID.ascending(), + aggregateID: input.sessionID, + seq: next, + type: SyncEvent.versionedType(Session.Event.Updated.type, Session.Event.Updated.version), + data: { + sessionID: input.sessionID, + info: { + workspaceID: input.workspaceID, + time: { + updated: Date.now(), + }, + }, + }, + }, + ] + + const size = 10 + const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size)) + const total = sets.length + log.info("session restore prepared", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + workspaceType: space.type, + directory: space.directory, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + events: all.length, + batches: total, + first: all[0]?.seq, + last: all.at(-1)?.seq, + }) + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: 0, + }, + }, + }) + for (const [i, events] of sets.entries()) { + log.info("session restore batch starting", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + }) + if (target.type === "local") { + SyncEvent.replayAll(events) + log.info("session restore batch replayed locally", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + }) + } else { + const url = route(target.url, "/sync/replay") + const headers = new Headers(target.headers) + headers.set("content-type", "application/json") + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + directory: space.directory ?? "", + events, + }), + }) + if (!res.ok) { + const body = await res.text() + log.error("session restore batch failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + body, + }) + throw new Error( + `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, + ) + } + log.info("session restore batch posted", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + }) + } + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: i + 1, + }, + }, + }) + } + + log.info("session restore complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + batches: total, + }) + + return { + total, + } + } catch (err) { + log.error("session restore failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + throw err + } + }) + export function list(project: Project.Info) { const rows = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + for (const space of spaces) startSync(space) return spaces } @@ -120,13 +304,25 @@ export namespace Workspace { }) export const remove = fn(WorkspaceID.zod, async (id) => { + const sessions = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), + ) + for (const session of sessions) { + await Session.remove(session.id) + } + const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) + if (row) { stopSync(id) const info = fromRow(row) - const adaptor = await getAdaptor(info.projectID, row.type) - adaptor.remove(info) + try { + const adaptor = await getAdaptor(info.projectID, row.type) + await adaptor.remove(info) + } catch (err) { + log.error("adaptor not available when removing workspace", { type: row.type }) + } Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) return info } @@ -156,47 +352,75 @@ export namespace Workspace { const log = Log.create({ service: "workspace-sync" }) - async function workspaceEventLoop(space: Info, signal: AbortSignal) { - log.info("starting sync: " + space.id) + function route(url: string | URL, path: string) { + const next = new URL(url) + next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` + next.search = "" + next.hash = "" + return next + } + async function globalEventLoop(space: Info, signal: AbortSignal) { while (!signal.aborted) { - log.info("connecting to sync: " + space.id) + console.log("connecting to global sync", { workspace: space.name }) - setStatus(space.id, "connecting") const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) if (target.type === "local") return - const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => { - setStatus(space.id, "error", String(err)) + const res = await fetch(route(target.url, "/global/event"), { + method: "GET", + headers: target.headers, + signal, + }).catch((err: unknown) => { + setStatus(space.id, "error") + + console.log("failed to connect to global sync", { + workspace: space.name, + error: err, + }) return undefined }) - if (!res || !res.ok || !res.body) { - log.info("failed to connect to sync: " + res?.status) - setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response") - await sleep(1000) + if (!res || !res.ok || !res.body) { + console.log("failed to connect to global sync", { workspace: space.name }) + await sleep(50000) continue } - setStatus(space.id, "connected") - await parseSSE(res.body, signal, (evt) => { - const event = evt as SyncEvent.SerializedEvent + console.log("global sync connected", { workspace: space.name }) + setStatus(space.id, "connected") + + await parseSSE(res.body, signal, (evt: any) => { try { - if (!event.type.startsWith("server.")) { - SyncEvent.replay(event) + if (!("payload" in evt)) return + + if (!evt.payload.type.startsWith("server.")) { + console.log("received workspace sse event", evt) } + + if (evt.payload.type === "sync") { + // This name -> type is temporary + SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent) + } + + GlobalBus.emit("event", { + directory: evt.directory, + project: evt.project, + workspace: space.id, + payload: evt.payload, + }) } catch (err) { - log.warn("failed to replay sync event", { + console.log("failed to replay global event", { workspaceID: space.id, error: err, }) } }) - setStatus(space.id, "disconnected") - log.info("disconnected to sync: " + space.id) - await sleep(250) + + log.info("disconnected from global sync: " + space.id) + await sleep(50000) } } @@ -213,9 +437,9 @@ export namespace Workspace { aborts.set(space.id, abort) setStatus(space.id, "disconnected") - void workspaceEventLoop(space, abort.signal).catch((error) => { + void globalEventLoop(space, abort.signal).catch((error) => { setStatus(space.id, "error", String(error)) - log.warn("workspace sync listener failed", { + log.warn("workspace listener failed", { workspaceID: space.id, error, }) diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index 634fe6aad0..ebb05ac319 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -103,6 +103,7 @@ export namespace PluginLoader { } catch (error) { return { ok: false, error } } + console.log('mod', mod) if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) } return { ok: true, value: { ...row, mod } } } diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 868131eb82..629c943321 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -11,9 +11,26 @@ import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" +import { Log } from "@/util/log" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } +const hop = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "host", + "content-length", +]) + +const IS_WORKSPACE = process.env.OPENCODE_WORKSPACE === "true" + const RULES: Array = [ { path: "/session/status", action: "forward" }, { method: "GET", path: "/session", action: "local" }, @@ -37,6 +54,40 @@ function getSessionID(url: URL) { return SessionID.make(id) } +function sh(value: string) { + return `'${value.replace(/'/g, `'"'"'`)}'` +} + +async function curl(url: URL, extra: HeadersInit | undefined, req: Request) { + const headers = new Headers(req.headers) + for (const key of hop) headers.delete(key) + headers.delete("accept-encoding") + headers.delete("x-opencode-directory") + headers.delete("x-opencode-workspace") + + if (extra) { + for (const [key, value] of new Headers(extra).entries()) { + headers.set(key, value) + } + } + + const parts = ["curl", "-X", req.method] + for (const [key, value] of headers.entries()) { + parts.push("-H", sh(`${key}: ${value}`)) + } + + if (req.method !== "GET" && req.method !== "HEAD") { + const body = await req + .clone() + .text() + .catch(() => "") + if (body) parts.push("--data-binary", sh(body)) + } + + parts.push(sh(url.toString())) + return parts.join(" ") +} + async function getSessionWorkspace(url: URL) { const id = getSessionID(url) if (!id) return null @@ -46,6 +97,8 @@ async function getSessionWorkspace(url: URL) { } export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler { + const log = Log.Default.clone().tag("service", "workspace-router") + return async (c, next) => { const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() const directory = Filesystem.resolve( @@ -64,7 +117,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace") // If no workspace is provided we use the project - if (!workspaceID) { + if (!workspaceID || url.pathname.startsWith("/console") || IS_WORKSPACE) { return Instance.provide({ directory, init: () => AppRuntime.runPromise(InstanceBootstrap), @@ -77,16 +130,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const workspace = await Workspace.get(WorkspaceID.make(workspaceID)) if (!workspace) { - // Special-case deleting a session in case user's data in a - // weird state. Allow them to forcefully delete a synced session - // even if the remote workspace is not in their data. - // - // The lets the `DELETE /session/:id` endpoint through and we've - // made sure that it will run without an instance - if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") { - return next() - } - return new Response(`Workspace not found: ${workspaceID}`, { status: 500, headers: { @@ -98,6 +141,13 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const adaptor = await getAdaptor(workspace.projectID, workspace.type) const target = await adaptor.target(workspace) + log.info("workspace route resolved", { + workspaceID, + workspace_type: workspace.type, + target_type: target.type, + request: url.toString(), + }) + if (target.type === "local") { return WorkspaceContext.provide({ workspaceID: WorkspaceID.make(workspaceID), @@ -118,18 +168,29 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware return next() } + const proxyURL = new URL(target.url) + proxyURL.pathname = `${proxyURL.pathname.replace(/\/$/, "")}${url.pathname}` + proxyURL.search = url.search + proxyURL.hash = url.hash + proxyURL.searchParams.delete("workspace") + + log.info("workspace proxy forwarding", { + workspaceID, + request: url.toString(), + target: String(target.url), + proxy: proxyURL.toString(), + }) + if (c.req.header("upgrade")?.toLowerCase() === "websocket") { - return ServerProxy.websocket(upgrade, target, c.req.raw, c.env) + return ServerProxy.websocket(upgrade, proxyURL, target.headers, c.req.raw, c.env) } const headers = new Headers(c.req.raw.headers) headers.delete("x-opencode-workspace") - return ServerProxy.http( - target, - new Request(c.req.raw, { - headers, - }), - ) + const req = new Request(c.req.raw, { headers }) + console.log("workspace proxy curl", await curl(proxyURL, target.headers, req)) + + return ServerProxy.http(proxyURL, target.headers, req) } } diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/instance/sync.ts new file mode 100644 index 0000000000..5efba7010f --- /dev/null +++ b/packages/opencode/src/server/instance/sync.ts @@ -0,0 +1,118 @@ +import z from "zod" +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import { SyncEvent } from "@/sync" +import { Database, asc, and, not, or, lte, eq } from "@/storage/db" +import { EventTable } from "@/sync/event.sql" +import { lazy } from "@/util/lazy" +import { Log } from "@/util/log" +import { errors } from "../error" + +const ReplayEvent = z.object({ + id: z.string(), + aggregateID: z.string(), + seq: z.number().int().min(0), + type: z.string(), + data: z.record(z.string(), z.unknown()), +}) + +const log = Log.create({ service: "server.sync" }) + +export const SyncRoutes = lazy(() => + new Hono() + .post( + "/replay", + describeRoute({ + summary: "Replay sync events", + description: "Validate and replay a complete sync event history.", + operationId: "global.sync-replay", + responses: { + 200: { + description: "Replayed sync events", + content: { + "application/json": { + schema: resolver( + z.object({ + sessionID: z.string(), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + directory: z.string(), + events: z.array(ReplayEvent).min(1), + }), + ), + async (c) => { + const body = c.req.valid("json") + const events = body.events + const source = events[0].aggregateID + log.info("sync replay requested", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + directory: body.directory, + }) + SyncEvent.replayAll(events) + + log.info("sync replay complete", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + }) + + return c.json({ + sessionID: source, + }) + }, + ) + .get( + "/history", + describeRoute({ + summary: "List sync events", + description: + "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + operationId: "global.sync-history.list", + responses: { + 200: { + description: "Sync events", + content: { + "application/json": { + schema: resolver( + z.array( + z.object({ + id: z.string(), + aggregate_id: z.string(), + seq: z.number(), + type: z.string(), + data: z.record(z.string(), z.unknown()), + }), + ), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", z.record(z.string(), z.number().int().min(0))), + async (c) => { + const body = c.req.valid("json") + const exclude = Object.entries(body) + const where = + exclude.length > 0 + ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!) + : undefined + const rows = Database.use((db) => db.select().from(EventTable).where(where).orderBy(asc(EventTable.seq)).all()) + return c.json(rows) + }, + ), +) diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index 7cee031975..f888ee57e3 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/instance/workspace.ts @@ -6,6 +6,16 @@ import { Workspace } from "../../control-plane/workspace" import { Instance } from "../../project/instance" import { errors } from "../error" import { lazy } from "../../util/lazy" +import { Log } from "@/util/log" +import { errorData } from "@/util/error" + +const SessionRestoreBody = Workspace.sessionRestore.schema.omit({ + workspaceID: true, +}) + +const SessionRestoreResult = z.object({ + total: z.number().int().min(0), +}) const WorkspaceAdaptor = z.object({ type: z.string(), @@ -13,6 +23,8 @@ const WorkspaceAdaptor = z.object({ description: z.string(), }) +const log = Log.create({ service: "server.workspace" }) + export const WorkspaceRoutes = lazy(() => new Hono() .get( @@ -140,5 +152,59 @@ export const WorkspaceRoutes = lazy(() => const { id } = c.req.valid("param") return c.json(await Workspace.remove(id)) }, + ) + .post( + "/:id/session-restore", + describeRoute({ + summary: "Restore session into workspace", + description: "Replay a session's sync events into the target workspace in batches.", + operationId: "experimental.workspace.sessionRestore", + responses: { + 200: { + description: "Session replay started", + content: { + "application/json": { + schema: resolver(SessionRestoreResult), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "param", + z.object({ + id: Workspace.Info.shape.id, + }), + ), + validator("json", SessionRestoreBody), + async (c) => { + const { id } = c.req.valid("param") + const body = c.req.valid("json") + log.info("session restore route requested", { + workspaceID: id, + sessionID: body.sessionID, + directory: Instance.directory, + }) + try { + const result = await Workspace.sessionRestore({ + workspaceID: id, + ...body, + }) + log.info("session restore route complete", { + workspaceID: id, + sessionID: body.sessionID, + total: result.total, + }) + return c.json(result) + } catch (err) { + log.error("session restore route failed", { + workspaceID: id, + sessionID: body.sessionID, + error: errorData(err), + }) + throw err + } + }, ), ) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index a51ba602b5..d0539eb247 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -86,7 +86,7 @@ const zipped = compress() export const CompressionMiddleware: MiddlewareHandler = (c, next) => { const path = c.req.path const method = c.req.method - if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next() + if (path === "/event" || path === "/global/event") return next() if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next() return zipped(c, next) } diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index c90a657dc2..0c0deba20c 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -1,6 +1,6 @@ -import type { Target } from "@/control-plane/types" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" +import { Log } from "@/util/log" const hop = new Set([ "connection", @@ -20,6 +20,7 @@ type Msg = string | ArrayBuffer | Uint8Array function headers(req: Request, extra?: HeadersInit) { const out = new Headers(req.headers) for (const key of hop) out.delete(key) + out.delete("accept-encoding") out.delete("x-opencode-directory") out.delete("x-opencode-workspace") if (!extra) return out @@ -98,31 +99,63 @@ const app = (upgrade: UpgradeWebSocket) => ) export namespace ServerProxy { - export function http(target: Extract, req: Request) { + const log = Log.Default.clone().tag("service", "server-proxy") + + export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) { + console.log("proxy http request", { + method: req.method, + request: req.url, + url: String(url), + }) return fetch( - new Request(target.url, { + new Request(url, { method: req.method, - headers: headers(req, target.headers), + headers: headers(req, extra), body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, redirect: "manual", signal: req.signal, }), - ) + ).then((res) => { + const next = new Headers(res.headers) + next.delete("content-encoding") + next.delete("content-length") + + console.log("proxy http response", { + method: req.method, + request: req.url, + url: String(url), + status: res.status, + statusText: res.statusText, + }) + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: next, + }) + }) } export function websocket( upgrade: UpgradeWebSocket, - target: Extract, + target: string | URL, + extra: HeadersInit | undefined, req: Request, env: unknown, ) { - const url = new URL(req.url) - url.pathname = "/__workspace_ws" - url.search = "" + const proxy = new URL(req.url) + proxy.pathname = "/__workspace_ws" + proxy.search = "" const next = new Headers(req.headers) - next.set("x-opencode-proxy-url", socket(target.url)) + next.set("x-opencode-proxy-url", socket(target)) + for (const [key, value] of new Headers(extra).entries()) { + next.set(key, value) + } + log.info("proxy websocket", { + request: req.url, + target: String(target), + }) return app(upgrade).fetch( - new Request(url, { + new Request(proxy, { method: req.method, headers: next, signal: req.signal, diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index d7cb7f774f..ce598dae67 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -199,6 +199,25 @@ export namespace SyncEvent { process(def, event, { publish: !!options?.publish }) } + export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { + const source = events[0]?.aggregateID + if (!source) return + if (events.some((item) => item.aggregateID !== source)) { + throw new Error("Replay events must belong to the same session") + } + const start = events[0].seq + for (const [i, item] of events.entries()) { + const seq = start + i + if (item.seq !== seq) { + throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`) + } + } + for (const item of events) { + replay(item, options) + } + return source + } + export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { const agg = (data as Record)[def.aggregate] // This should never happen: we've enforced it via typescript in diff --git a/packages/opencode/test/cli/tui/workspace-restore.test.ts b/packages/opencode/test/cli/tui/workspace-restore.test.ts new file mode 100644 index 0000000000..e47ed4ccb7 --- /dev/null +++ b/packages/opencode/test/cli/tui/workspace-restore.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, mock, test } from "bun:test" +import { restoreWorkspaceSession } from "../../../src/cli/cmd/tui/component/dialog-workspace-create" + +describe("restoreWorkspaceSession", () => { + test("refreshes workspace and session data after a successful restore", async () => { + const sessionRestore = mock(async () => ({ data: { total: 2 } })) + const syncWorkspace = mock(async () => {}) + const refresh = mock(async () => {}) + const clear = mock(() => {}) + const show = mock(() => {}) + + await restoreWorkspaceSession({ + dialog: { clear } as any, + sdk: { + client: { + experimental: { + workspace: { + sessionRestore, + }, + }, + }, + } as any, + sync: { + session: { + refresh, + }, + } as any, + project: { + workspace: { + sync: syncWorkspace, + }, + } as any, + toast: { show } as any, + workspaceID: "wrk_1", + sessionID: "ses_1", + }) + + expect(sessionRestore).toHaveBeenCalledWith({ id: "wrk_1", sessionID: "ses_1" }) + expect(syncWorkspace).toHaveBeenCalledTimes(1) + expect(refresh).toHaveBeenCalledTimes(1) + expect(show).toHaveBeenCalledWith({ + message: "Session restored into the new workspace", + variant: "success", + }) + expect(clear).toHaveBeenCalledTimes(1) + }) + + test("shows an error and keeps the dialog open when restore fails", async () => { + const sessionRestore = mock(async () => undefined) + const syncWorkspace = mock(async () => {}) + const refresh = mock(async () => {}) + const clear = mock(() => {}) + const show = mock(() => {}) + + await restoreWorkspaceSession({ + dialog: { clear } as any, + sdk: { + client: { + experimental: { + workspace: { + sessionRestore, + }, + }, + }, + } as any, + sync: { + session: { + refresh, + }, + } as any, + project: { + workspace: { + sync: syncWorkspace, + }, + } as any, + toast: { show } as any, + workspaceID: "wrk_1", + sessionID: "ses_1", + }) + + expect(syncWorkspace).not.toHaveBeenCalled() + expect(refresh).not.toHaveBeenCalled() + expect(clear).not.toHaveBeenCalled() + expect(show).toHaveBeenCalledWith({ + message: "Failed to restore session: no response", + variant: "error", + }) + }) +}) diff --git a/packages/opencode/test/server/workspace-restore.test.ts b/packages/opencode/test/server/workspace-restore.test.ts new file mode 100644 index 0000000000..917ee193b6 --- /dev/null +++ b/packages/opencode/test/server/workspace-restore.test.ts @@ -0,0 +1,268 @@ +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" +import fs from "node:fs/promises" +import path from "node:path" +import { GlobalBus } from "../../src/bus/global" +import { registerAdaptor } from "../../src/control-plane/adaptors" +import type { WorkspaceAdaptor } from "../../src/control-plane/types" +import { Workspace } from "../../src/control-plane/workspace" +import { Flag } from "../../src/flag/flag" +import { ModelID, ProviderID } from "../../src/provider/schema" +import { Instance } from "../../src/project/instance" +import { Server } from "../../src/server/server" +import { Session } from "../../src/session" +import { MessageID, PartID, type SessionID } from "../../src/session/schema" +import { Database, asc, eq } from "../../src/storage/db" +import { SyncEvent } from "../../src/sync" +import { EventTable } from "../../src/sync/event.sql" +import { Log } from "../../src/util/log" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES + +beforeEach(() => { + Database.close() + // @ts-expect-error test override + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true +}) + +afterEach(async () => { + mock.restore() + // @ts-expect-error test override + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original + await resetDatabase() +}) + +async function user(sessionID: SessionID, text: string) { + const msg = await Session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID, + agent: "build", + model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + time: { created: Date.now() }, + }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID, + messageID: msg.id, + type: "text", + text, + }) +} + +function remote(dir: string, url: string): WorkspaceAdaptor { + return { + name: "remote", + description: "remote", + configure(info) { + return { + ...info, + directory: dir, + } + }, + async create() { + await fs.mkdir(dir, { recursive: true }) + }, + async remove() {}, + target() { + return { + type: "remote" as const, + url, + } + }, + } +} + +function local(dir: string): WorkspaceAdaptor { + return { + name: "local", + description: "local", + configure(info) { + return { + ...info, + directory: dir, + } + }, + async create() { + await fs.mkdir(dir, { recursive: true }) + }, + async remove() {}, + target() { + return { + type: "local" as const, + directory: dir, + } + }, + } +} + +describe("workspace restore route", () => { + test("replays session events in batches of 10 and emits progress", async () => { + await using tmp = await tmpdir({ git: true }) + const app = Server.Default().app + const dir = path.join(tmp.path, ".restore") + const seen: any[] = [] + const posts: Array<{ + path: string + body: { directory: string; events: Array<{ seq: number; aggregateID: string }> } + }> = [] + const on = (evt: any) => seen.push(evt) + GlobalBus.on("event", on) + + const raw = globalThis.fetch + const fetch = spyOn(globalThis, "fetch").mockImplementation( + Object.assign( + async (input: URL | RequestInfo, init?: BunFetchRequestInit | RequestInit) => { + const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url) + posts.push({ + path: url.pathname, + body: JSON.parse(String(init?.body)), + }) + return Response.json({ sessionID: posts.at(-1)!.body.events[0].aggregateID }) + }, + { + preconnect: raw.preconnect?.bind(raw), + }, + ) as typeof globalThis.fetch, + ) + + try { + const setup = await Instance.provide({ + directory: tmp.path, + fn: async () => { + registerAdaptor(Instance.project.id, "worktree", remote(dir, "https://workspace.test/base")) + const space = await Workspace.create({ + type: "worktree", + branch: null, + extra: null, + projectID: Instance.project.id, + }) + const session = await Session.create({}) + for (let i = 0; i < 6; i++) { + await user(session.id, `msg ${i}`) + } + const rows = Database.use((db) => + db + .select({ seq: EventTable.seq }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, session.id)) + .orderBy(asc(EventTable.seq)) + .all(), + ) + return { space, session, rows } + }, + }) + + expect(setup.rows).toHaveLength(13) + + const res = await app.request(`/experimental/workspace/${setup.space.id}/session-restore`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ + sessionID: setup.session.id, + }), + }) + + expect(fetch).toHaveBeenCalledTimes(2) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ total: 2 }) + expect(posts).toHaveLength(2) + expect(posts[0]?.path).toBe("/base/sync/replay") + expect(posts[1]?.path).toBe("/base/sync/replay") + expect(posts[0]?.body.directory).toBe(dir) + expect(posts[1]?.body.directory).toBe(dir) + expect(posts[0]?.body.events).toHaveLength(10) + expect(posts[1]?.body.events).toHaveLength(4) + expect(posts.flatMap((item) => item.body.events.map((event) => event.seq))).toEqual([ + ...setup.rows.map((row) => row.seq), + setup.rows.at(-1)!.seq + 1, + ]) + expect(posts[1]?.body.events.at(-1)).toMatchObject({ + aggregateID: setup.session.id, + seq: setup.rows.at(-1)!.seq + 1, + type: SyncEvent.versionedType(Session.Event.Updated.type, Session.Event.Updated.version), + data: { + sessionID: setup.session.id, + info: { + workspaceID: setup.space.id, + }, + }, + }) + + const restore = seen.filter( + (evt) => evt.workspace === setup.space.id && evt.payload.type === Workspace.Event.Restore.type, + ) + expect(restore.map((evt) => evt.payload.properties.step)).toEqual([0, 1, 2]) + expect(restore.map((evt) => evt.payload.properties.total)).toEqual([2, 2, 2]) + expect(restore.map((evt) => evt.payload.properties.sessionID)).toEqual([ + setup.session.id, + setup.session.id, + setup.session.id, + ]) + } finally { + GlobalBus.off("event", on) + } + }) + + test("replays locally without posting to a server", async () => { + await using tmp = await tmpdir({ git: true }) + const app = Server.Default().app + const dir = path.join(tmp.path, ".restore-local") + const seen: any[] = [] + const on = (evt: any) => seen.push(evt) + GlobalBus.on("event", on) + + const fetch = spyOn(globalThis, "fetch") + const replayAll = spyOn(SyncEvent, "replayAll") + + try { + const setup = await Instance.provide({ + directory: tmp.path, + fn: async () => { + registerAdaptor(Instance.project.id, "local-restore", local(dir)) + const space = await Workspace.create({ + type: "local-restore", + branch: null, + extra: null, + projectID: Instance.project.id, + }) + const session = await Session.create({}) + for (let i = 0; i < 6; i++) { + await user(session.id, `msg ${i}`) + } + return { space, session } + }, + }) + + const res = await app.request(`/experimental/workspace/${setup.space.id}/session-restore`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ + sessionID: setup.session.id, + }), + }) + + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ total: 2 }) + expect(fetch).not.toHaveBeenCalled() + expect(replayAll).toHaveBeenCalledTimes(2) + expect((await Session.get(setup.session.id)).workspaceID).toBe(setup.space.id) + + const restore = seen.filter( + (evt) => evt.workspace === setup.space.id && evt.payload.type === Workspace.Event.Restore.type, + ) + expect(restore.map((evt) => evt.payload.properties.step)).toEqual([0, 1, 2]) + } finally { + GlobalBus.off("event", on) + } + }) +}) diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 5304f4ea8b..1fb69950ca 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -9,6 +9,7 @@ import { EventTable } from "../../src/sync/event.sql" import { Identifier } from "../../src/id/id" import { Flag } from "../../src/flag/flag" import { initProjectors } from "../../src/server/projectors" +import { SyncRoutes } from "../../src/server/routes/sync" const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES @@ -187,5 +188,74 @@ describe("SyncEvent", () => { ).toThrow(/Unknown event type/) }), ) + + test( + "sync route accepts later chunks after the first batch", + withInstance(async () => { + const { Created } = setup() + const app = SyncRoutes() + const id = Identifier.descending("message") + + const one = await app.request("/replay", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + directory: "/tmp/test", + events: [ + { + id: "evt_1", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 0, + aggregateID: id, + data: { id, name: "first" }, + }, + { + id: "evt_2", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 1, + aggregateID: id, + data: { id, name: "second" }, + }, + ], + }), + }) + + const two = await app.request("/replay", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + directory: "/tmp/test", + events: [ + { + id: "evt_3", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 2, + aggregateID: id, + data: { id, name: "third" }, + }, + { + id: "evt_4", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 3, + aggregateID: id, + data: { id, name: "fourth" }, + }, + ], + }), + }) + + expect(one.status).toBe(200) + expect(await one.json()).toEqual({ sessionID: id }) + expect(two.status).toBe(200) + expect(await two.json()).toEqual({ sessionID: id }) + + const rows = Database.use((db) => db.select().from(EventTable).all()) + expect(rows.map((row) => row.seq)).toEqual([0, 1, 2, 3]) + }), + ) }) }) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b5fc976bba..07a1e81012 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -35,6 +35,8 @@ import type { ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ExperimentalWorkspaceSessionRestoreResponses, ExperimentalWorkspaceStatusResponses, FileListResponses, FilePartInput, @@ -51,6 +53,11 @@ import type { GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, + GlobalSyncEventSubscribeResponses, + GlobalSyncHistoryListErrors, + GlobalSyncHistoryListResponses, + GlobalSyncReplayErrors, + GlobalSyncReplayResponses, GlobalUpgradeErrors, GlobalUpgradeResponses, InstanceDisposeResponses, @@ -236,6 +243,20 @@ class HeyApiRegistry { } } +export class SyncEvent extends HeyApiClient { + /** + * Subscribe to global sync events + * + * Get global sync events + */ + public subscribe(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/global/sync-event", + ...options, + }) + } +} + export class Config extends HeyApiClient { /** * Get global configuration @@ -274,6 +295,38 @@ export class Config extends HeyApiClient { } } +export class SyncHistory extends HeyApiClient { + /** + * List sync events + * + * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. + */ + public list( + parameters?: { + body?: { + [key: string]: number + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "body", map: "body" }] }]) + return (options?.client ?? this.client).get< + GlobalSyncHistoryListResponses, + GlobalSyncHistoryListErrors, + ThrowOnError + >({ + url: "/sync/history", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Global extends HeyApiClient { /** * Get health @@ -335,10 +388,63 @@ export class Global extends HeyApiClient { }) } + /** + * Replay sync events + * + * Validate and replay a complete sync event history. + */ + public syncReplay( + parameters?: { + directory?: string + events?: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "body", key: "directory" }, + { in: "body", key: "events" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/replay", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _syncEvent?: SyncEvent + get syncEvent(): SyncEvent { + return (this._syncEvent ??= new SyncEvent({ client: this.client })) + } + private _config?: Config get config(): Config { return (this._config ??= new Config({ client: this.client })) } + + private _syncHistory?: SyncHistory + get syncHistory(): SyncHistory { + return (this._syncHistory ??= new SyncHistory({ client: this.client })) + } } export class Auth extends HeyApiClient { @@ -1243,6 +1349,49 @@ export class Workspace extends HeyApiClient { }) } + /** + * Restore session into workspace + * + * Replay a session's sync events into the target workspace in batches. + */ + public sessionRestore( + parameters: { + id: string + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}/session-restore", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + private _adaptor?: Adaptor get adaptor(): Adaptor { return (this._adaptor ??= new Adaptor({ client: this.client })) @@ -1743,7 +1892,6 @@ export class Session2 extends HeyApiClient { directory?: string workspace?: string title?: string - permission?: PermissionRuleset time?: { archived?: number } @@ -1759,7 +1907,6 @@ export class Session2 extends HeyApiClient { { in: "query", key: "directory" }, { in: "query", key: "workspace" }, { in: "body", key: "title" }, - { in: "body", key: "permission" }, { in: "body", key: "time" }, ], }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e2a9a88ad3..8cb37e050d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -33,13 +33,6 @@ export type EventProjectUpdated = { properties: Project } -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - export type EventInstallationUpdated = { type: "installation.updated" properties: { @@ -54,6 +47,13 @@ export type EventInstallationUpdateAvailable = { } } +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + export type EventServerConnected = { type: "server.connected" properties: { @@ -68,21 +68,6 @@ export type EventGlobalDisposed = { } } -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type EventLspClientDiagnostics = { type: "lsp.client.diagnostics" properties: { @@ -230,133 +215,25 @@ export type EventSessionError = { } } -export type QuestionOption = { - /** - * Display text (1-5 words, concise) - */ - label: string - /** - * Explanation of choice - */ - description: string -} - -export type QuestionInfo = { - /** - * Complete question - */ - question: string - /** - * Very short label (max 30 chars) - */ - header: string - /** - * Available choices - */ - options: Array - /** - * Allow selecting multiple choices - */ - multiple?: boolean - /** - * Allow typing a custom answer (default: true) - */ - custom?: boolean -} - -export type QuestionRequest = { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: { - messageID: string - callID: string +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string } } -export type EventQuestionAsked = { - type: "question.asked" - properties: QuestionRequest -} - -export type QuestionAnswer = Array - -export type EventQuestionReplied = { - type: "question.replied" +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" properties: { - sessionID: string - requestID: string - answers: Array + file: string + event: "add" | "change" | "unlink" } } -export type EventQuestionRejected = { - type: "question.rejected" +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" properties: { - sessionID: string - requestID: string - } -} - -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string + branch?: string } } @@ -439,28 +316,169 @@ export type EventCommandExecuted = { } } -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventWorktreeReady = { - type: "worktree.ready" +export type EventWorkspaceReady = { + type: "workspace.ready" properties: { name: string - branch: string } } -export type EventWorktreeFailed = { - type: "worktree.failed" +export type EventWorkspaceFailed = { + type: "workspace.failed" properties: { message: string } } +export type EventWorkspaceRestore = { + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number + } +} + +export type EventWorkspaceStatus = { + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + } +} + +export type QuestionOption = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionInfo = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + /** + * Allow selecting multiple choices + */ + multiple?: boolean + /** + * Allow typing a custom answer (default: true) + */ + custom?: boolean +} + +export type QuestionRequest = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} + +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type Pty = { id: string title: string @@ -500,29 +518,21 @@ export type EventPtyDeleted = { } } -export type EventWorkspaceReady = { - type: "workspace.ready" +export type EventWorktreeReady = { + type: "worktree.ready" properties: { name: string + branch: string } } -export type EventWorkspaceFailed = { - type: "workspace.failed" +export type EventWorktreeFailed = { + type: "worktree.failed" properties: { message: string } } -export type EventWorkspaceStatus = { - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - error?: string - } -} - export type OutputFormatText = { type: "text" } @@ -971,12 +981,65 @@ export type EventSessionDeleted = { } } +export type Event = + | EventProjectUpdated + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventServerInstanceDisposed + | EventServerConnected + | EventGlobalDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventFileEdited + | EventFileWatcherUpdated + | EventVcsBranchUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTodoUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorktreeReady + | EventWorktreeFailed + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + +export type GlobalEvent = { + directory: string + project?: string + workspace?: string + payload: Event +} + export type SyncEventMessageUpdated = { - type: "sync" - name: "message.updated.1" - id: string - seq: number - aggregateID: "sessionID" + type: "message.updated.1" + aggregate: "sessionID" data: { sessionID: string info: Message @@ -984,11 +1047,8 @@ export type SyncEventMessageUpdated = { } export type SyncEventMessageRemoved = { - type: "sync" - name: "message.removed.1" - id: string - seq: number - aggregateID: "sessionID" + type: "message.removed.1" + aggregate: "sessionID" data: { sessionID: string messageID: string @@ -996,11 +1056,8 @@ export type SyncEventMessageRemoved = { } export type SyncEventMessagePartUpdated = { - type: "sync" - name: "message.part.updated.1" - id: string - seq: number - aggregateID: "sessionID" + type: "message.part.updated.1" + aggregate: "sessionID" data: { sessionID: string part: Part @@ -1009,11 +1066,8 @@ export type SyncEventMessagePartUpdated = { } export type SyncEventMessagePartRemoved = { - type: "sync" - name: "message.part.removed.1" - id: string - seq: number - aggregateID: "sessionID" + type: "message.part.removed.1" + aggregate: "sessionID" data: { sessionID: string messageID: string @@ -1022,11 +1076,8 @@ export type SyncEventMessagePartRemoved = { } export type SyncEventSessionCreated = { - type: "sync" - name: "session.created.1" - id: string - seq: number - aggregateID: "sessionID" + type: "session.created.1" + aggregate: "sessionID" data: { sessionID: string info: Session @@ -1034,11 +1085,8 @@ export type SyncEventSessionCreated = { } export type SyncEventSessionUpdated = { - type: "sync" - name: "session.updated.1" - id: string - seq: number - aggregateID: "sessionID" + type: "session.updated.1" + aggregate: "sessionID" data: { sessionID: string info: { @@ -1077,75 +1125,16 @@ export type SyncEventSessionUpdated = { } export type SyncEventSessionDeleted = { - type: "sync" - name: "session.deleted.1" - id: string - seq: number - aggregateID: "sessionID" + type: "session.deleted.1" + aggregate: "sessionID" data: { sessionID: string info: Session } } -export type GlobalEvent = { - directory: string - project?: string - workspace?: string - payload: - | EventProjectUpdated - | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventVcsBranchUpdated - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | SyncEventMessageUpdated - | SyncEventMessageRemoved - | SyncEventMessagePartUpdated - | SyncEventMessagePartRemoved - | SyncEventSessionCreated - | SyncEventSessionUpdated - | SyncEventSessionDeleted +export type SyncEvent = { + payload: SyncEvent } /** @@ -2004,54 +1993,6 @@ export type File = { status: "added" | "deleted" | "modified" } -export type Event = - | EventProjectUpdated - | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventVcsBranchUpdated - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - export type McpStatusConnected = { status: "connected" } @@ -2183,6 +2124,23 @@ export type GlobalEventResponses = { export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] +export type GlobalSyncEventSubscribeData = { + body?: never + path?: never + query?: never + url: "/global/sync-event" +} + +export type GlobalSyncEventSubscribeResponses = { + /** + * Event stream + */ + 200: SyncEvent +} + +export type GlobalSyncEventSubscribeResponse = + GlobalSyncEventSubscribeResponses[keyof GlobalSyncEventSubscribeResponses] + export type GlobalConfigGetData = { body?: never path?: never @@ -2275,6 +2233,79 @@ export type GlobalUpgradeResponses = { export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] +export type GlobalSyncReplayData = { + body?: { + directory: string + events: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + } + path?: never + query?: never + url: "/sync/replay" +} + +export type GlobalSyncReplayErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalSyncReplayError = GlobalSyncReplayErrors[keyof GlobalSyncReplayErrors] + +export type GlobalSyncReplayResponses = { + /** + * Replayed sync events + */ + 200: { + sessionID: string + } +} + +export type GlobalSyncReplayResponse = GlobalSyncReplayResponses[keyof GlobalSyncReplayResponses] + +export type GlobalSyncHistoryListData = { + body?: { + [key: string]: number + } + path?: never + query?: never + url: "/sync/history" +} + +export type GlobalSyncHistoryListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalSyncHistoryListError = GlobalSyncHistoryListErrors[keyof GlobalSyncHistoryListErrors] + +export type GlobalSyncHistoryListResponses = { + /** + * Sync events + */ + 200: Array<{ + id: string + aggregate_id: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> +} + +export type GlobalSyncHistoryListResponse = GlobalSyncHistoryListResponses[keyof GlobalSyncHistoryListResponses] + export type AuthRemoveData = { body?: never path: { @@ -3000,6 +3031,42 @@ export type ExperimentalWorkspaceRemoveResponses = { export type ExperimentalWorkspaceRemoveResponse = ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] +export type ExperimentalWorkspaceSessionRestoreData = { + body?: { + sessionID: string + } + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}/session-restore" +} + +export type ExperimentalWorkspaceSessionRestoreErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceSessionRestoreError = + ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] + +export type ExperimentalWorkspaceSessionRestoreResponses = { + /** + * Session replay started + */ + 200: { + total: number + } +} + +export type ExperimentalWorkspaceSessionRestoreResponse = + ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] + export type WorktreeRemoveData = { body?: WorktreeRemoveInput path?: never @@ -3343,7 +3410,6 @@ export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] export type SessionUpdateData = { body?: { title?: string - permission?: PermissionRuleset time?: { archived?: number } diff --git a/sync-routes.ts b/sync-routes.ts new file mode 100644 index 0000000000..1c67f02e35 --- /dev/null +++ b/sync-routes.ts @@ -0,0 +1,189 @@ +import z from "zod" +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import { SyncEvent } from "@/sync" +import { Database, asc, and, not, or, lte, eq } from "@/storage/db" +import { EventTable } from "@/sync/event.sql" +import { Log } from "@/util/log" +import { lazy } from "@/util/lazy" +import { Instance } from "@/project/instance" +import { InstanceBootstrap } from "../../project/bootstrap" +import { errors } from "../error" +import { streamQueue } from "../stream-queue" + +const log = Log.create({ service: "server" }) + +const ReplayEvent = z.object({ + id: z.string(), + aggregateID: z.string(), + seq: z.number().int().min(0), + type: z.string(), + data: z.record(z.string(), z.unknown()), +}) + +export const SyncRoutes = lazy(() => + new Hono() + .get( + "/event", + describeRoute({ + summary: "Subscribe to sync events", + description: "Get sync events", + operationId: "sync.event", + responses: { + 200: { + description: "Event stream", + content: { + "text/event-stream": { + schema: resolver( + z + .object({ + payload: SyncEvent.payloads(), + }) + .meta({ + ref: "SyncEvent", + }), + ), + }, + }, + }, + }, + }), + async (c) => { + log.info("sync event connected") + c.header("X-Accel-Buffering", "no") + c.header("X-Content-Type-Options", "nosniff") + return streamQueue(c, { + connect: (q) => { + log.info("sync event connected") + + q.push( + JSON.stringify({ + type: "server.connected", + properties: {}, + }), + ) + }, + heartbeat: (q) => { + q.push( + JSON.stringify({ + type: "server.heartbeat", + properties: {}, + }), + ) + }, + + subscribe: (q) => { + const unsub = SyncEvent.subscribeAll(({ def, event }) => { + q.push(JSON.stringify({ ...event, type: SyncEvent.versionedType(def.type, def.version) })) + }) + + return () => { + unsub() + log.info("sync event disconnected") + } + }, + }) + }, + ) + .post( + "/replay", + describeRoute({ + summary: "Replay sync events", + description: "Validate and replay a complete sync event history.", + operationId: "global.sync-replay", + responses: { + 200: { + description: "Replayed sync events", + content: { + "application/json": { + schema: resolver( + z.object({ + sessionID: z.string(), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + directory: z.string(), + events: z.array(ReplayEvent).min(1), + }), + ), + async (c) => { + const body = c.req.valid("json") + const events = body.events + const source = events[0].aggregateID + if (events.some((item) => item.aggregateID !== source)) { + throw new Error("Replay events must belong to the same session") + } + for (const [i, item] of events.entries()) { + if (item.seq !== i) throw new Error(`Replay sequence mismatch at index ${i}: expected ${i}, got ${item.seq}`) + } + + return Instance.provide({ + directory: body.directory, + init: InstanceBootstrap, + async fn() { + for (const item of events) { + SyncEvent.replay(item) + } + return c.json({ sessionID: source }) + }, + }) + }, + ) + .get( + "/history", + describeRoute({ + summary: "List sync events", + description: "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + operationId: "global.sync-history.list", + responses: { + 200: { + description: "Sync events", + content: { + "application/json": { + schema: resolver( + z.array( + z.object({ + id: z.string(), + aggregate_id: z.string(), + seq: z.number(), + type: z.string(), + data: z.record(z.string(), z.unknown()), + }), + ), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.record(z.string(), z.number().int().min(0)), + ), + async (c) => { + const body = c.req.valid("json") + const exclude = Object.entries(body) + const where = exclude.length > 0 + ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!) + : undefined + const rows = Database.use((db) => + db + .select() + .from(EventTable) + .where(where) + .orderBy(asc(EventTable.seq)) + .all(), + ) + return c.json(rows) + }, + ), +)