diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index 30cc94bd540..e962e782779 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,5 +1,4 @@ import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import { iife } from "@opencode-ai/core/util/iife" import type { SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise" import z from "zod" import { Storage } from "./storage" @@ -210,38 +209,7 @@ export namespace Share { const share = await get(input.share.id) if (!share) throw new Errors.NotFound(input.share.id) if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id) - const promises = [] - for (const item of input.data) { - promises.push( - iife(async () => { - switch (item.type) { - case "session": - await Storage.write(["share_data", input.share.id, "session"], item.data) - break - case "message": { - const data = item.data as Message - await Storage.write(["share_data", input.share.id, "message", data.id], item.data) - break - } - case "messages": - await Storage.write(["share_data", input.share.id, "messages", item.data.sessionID], item.data) - break - case "part": { - const data = item.data as Part - await Storage.write(["share_data", input.share.id, "part", data.messageID, data.id], item.data) - break - } - case "session_diff": - await Storage.write(["share_data", input.share.id, "session_diff"], item.data) - break - case "model": - await Storage.write(["share_data", input.share.id, "model"], item.data) - break - } - }), - ) - } - await Promise.all(promises) + await Promise.all(input.data.map((item) => Storage.write(["share_data", input.share.id, key(item)], item.data))) }, ) diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index 35d52735e2e..b81da94da45 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -289,4 +289,52 @@ describe.concurrent("core.share", () => { await Share.remove({ id: share.id, secret: share.secret }) }) + + test("should sync all legacy data variants to their canonical paths", async () => { + const sessionID = Identifier.descending() + const share = await Share.create({ sessionID }) + const data: Share.Data[] = [ + { + type: "session", + data: { + id: sessionID, + slug: "session", + projectID: "project", + directory: "/", + title: "Session", + version: "1", + time: { created: 1, updated: 1 }, + }, + }, + { + type: "message", + data: { + id: "msg1", + sessionID, + role: "user", + time: { created: 1 }, + agent: "build", + model: { providerID: "provider", modelID: "model" }, + }, + }, + { type: "messages", data: { sessionID, messages: [] } }, + { + type: "part", + data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" }, + }, + { type: "session_diff", data: [] }, + { type: "model", data: [] }, + ] + + await Share.syncOld({ + share: { id: share.id, secret: share.secret }, + data, + }) + + const paths = ["session", "message/msg1", `messages/${sessionID}`, "part/msg1/part1", "session_diff", "model"] + const stored = await Promise.all(paths.map((path) => Storage.read(["share_data", share.id, ...path.split("/")]))) + expect(stored).toEqual(data.map((item) => item.data)) + + await Share.remove({ id: share.id, secret: share.secret }) + }) }) diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index bd5523a2a04..aa1f7996d2d 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -19,7 +19,7 @@ const opencode = await createOpencode({ }) console.log("✅ Opencode server ready") -const sessions = new Map() +const sessions = new Map() void (async () => { const events = await opencode.client.event.subscribe() for await (const event of events.stream) { @@ -27,7 +27,7 @@ void (async () => { const part = event.properties.part if (part.type === "tool") { // Find the session for this tool update - for (const [_sessionKey, session] of sessions.entries()) { + for (const session of sessions.values()) { if (session.sessionId === part.sessionID) { void handleToolUpdate(part, session.channel, session.thread) break @@ -66,16 +66,14 @@ app.message(async ({ message, say }) => { console.log("✅ Processing message:", message.text) const channel = message.channel - const thread = (message as any).thread_ts || message.ts + const thread = ("thread_ts" in message && typeof message.thread_ts === "string" && message.thread_ts) || message.ts const sessionKey = `${channel}-${thread}` let session = sessions.get(sessionKey) if (!session) { console.log("🆕 Creating new opencode session...") - const { client, server } = opencode - - const createResult = await client.session.create({ + const createResult = await opencode.client.session.create({ body: { title: `Slack thread ${thread}` }, }) @@ -90,10 +88,10 @@ app.message(async ({ message, say }) => { console.log("✅ Created opencode session:", createResult.data.id) - session = { client, server, sessionId: createResult.data.id, channel, thread } + session = { sessionId: createResult.data.id, channel, thread } sessions.set(sessionKey, session) - const shareResult = await client.session.share({ path: { id: createResult.data.id } }) + const shareResult = await opencode.client.session.share({ path: { id: createResult.data.id } }) if (!shareResult.error && shareResult.data) { const sessionUrl = shareResult.data.share?.url console.log("🔗 Session shared:", sessionUrl) @@ -102,7 +100,7 @@ app.message(async ({ message, say }) => { } console.log("📝 Sending to opencode:", message.text) - const result = await session.client.session.prompt({ + const result = await opencode.client.session.prompt({ path: { id: session.sessionId }, body: { parts: [{ type: "text", text: message.text }] }, }) @@ -118,16 +116,12 @@ app.message(async ({ message, say }) => { return } - const response = result.data - // Build response text const responseText = - response.info?.content || - response.parts - ?.filter((p: any) => p.type === "text") - .map((p: any) => p.text) - .join("\n") || - "I received your message but didn't have a response." + result.data.parts + .filter((part) => part.type === "text") + .map((part) => part.text) + .join("\n") || "I received your message but didn't have a response." console.log("💬 Sending response:", responseText) diff --git a/packages/updates/src/index.test.ts b/packages/updates/src/index.test.ts index ae6af0d26d8..5d5e7ebf91d 100644 --- a/packages/updates/src/index.test.ts +++ b/packages/updates/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { channelsForRef, resolveChannel, validGitHubClaims } from "./index" +import worker, { channelsForRef, resolveChannel, validGitHubClaims } from "./index" const claims = { repository: "anomalyco/opencode", @@ -42,3 +42,69 @@ test("routes the retired next channel to beta", () => { expect(resolveChannel("next")).toBe("beta") expect(resolveChannel("dev")).toBe("dev") }) + +const artifact = { + channel: "beta", + name: "opencode", + distribution: "darwin-arm64", + version: "1.0.0", + metadata: "{}", + active: 1, + time_created: 1, + time_updated: 2, +} + +test.each([ + ["/api/next", ["beta"], { channel: "beta", artifacts: [{ ...artifact, metadata: {}, active: true }] }], + [ + "/api/next/opencode", + ["beta", "opencode"], + { channel: "beta", name: "opencode", artifacts: [{ ...artifact, metadata: {}, active: true }] }, + ], + [ + "/api/next/opencode/darwin-arm64", + ["beta", "opencode", "darwin-arm64"], + { ...artifact, metadata: {}, active: true }, + ], +])("routes GET %s", async (path, expectedBindings, expectedBody) => { + const bindings: unknown[][] = [] + const statement = { + bind(...values: unknown[]) { + bindings.push(values) + return statement + }, + async all() { + return { results: [artifact] } + }, + async first() { + return artifact + }, + } + const db = { + prepare() { + return statement + }, + } as unknown as D1Database + + const response = await worker.fetch(new Request(`https://update.opencode.ai${path}`), { DB: db }) + + expect(response.status).toBe(200) + expect(await response.text()).toBe(JSON.stringify(expectedBody)) + expect(bindings).toEqual([expectedBindings]) +}) + +test.each(["/api", "/v1/dev", "/api/dev/opencode/darwin-arm64/extra", "/api/dev/opencode/darwin$arm64"])( + "returns 404 for GET %s", + async (path) => { + const db = { + prepare() { + throw new Error("Invalid routes must not query the database") + }, + } as unknown as D1Database + + const response = await worker.fetch(new Request(`https://update.opencode.ai${path}`), { DB: db }) + + expect(response.status).toBe(404) + expect(await response.text()).toBe("Not found") + }, +) diff --git a/packages/updates/src/index.ts b/packages/updates/src/index.ts index e166c75ab39..fdef7ecfa36 100644 --- a/packages/updates/src/index.ts +++ b/packages/updates/src/index.ts @@ -31,7 +31,7 @@ const audience = "https://update.opencode.ai" const githubKeys = createRemoteJWKSet(new URL("https://token.actions.githubusercontent.com/.well-known/jwks")) export default { - async fetch(request, env): Promise { + async fetch(request: Request, env: Env): Promise { const url = new URL(request.url) if (url.pathname === "/") return json({ service: "opencode-updates" }) @@ -40,28 +40,15 @@ export default { if (url.pathname === "/api/publish" && request.method === "POST") return publishArtifact(request, env) if (request.method !== "GET") return new Response("Method not allowed", { status: 405 }) - const segments = url.pathname.split("/").filter(Boolean) - if (segments.length === 2 && segments[0] === "api" && validIdentifier(segments[1])) { - return channel(env.DB, resolveChannel(segments[1])) + const [root, ...path] = url.pathname.split("/").filter(Boolean) + if (root !== "api" || path.length < 1 || path.length > 3 || !path.every(validIdentifier)) { + return new Response("Not found", { status: 404 }) } - if ( - segments.length === 3 && - segments[0] === "api" && - validIdentifier(segments[1]) && - validIdentifier(segments[2]) - ) { - return artifactName(env.DB, resolveChannel(segments[1]), segments[2]) - } - if ( - segments.length === 4 && - segments[0] === "api" && - validIdentifier(segments[1]) && - validIdentifier(segments[2]) && - validIdentifier(segments[3]) - ) { - return artifactDistribution(env.DB, resolveChannel(segments[1]), segments[2], segments[3]) - } - return new Response("Not found", { status: 404 }) + + const resolved = resolveChannel(path[0]) + if (path.length === 1) return channel(env.DB, resolved) + if (path.length === 2) return artifactName(env.DB, resolved, path[1]) + return artifactDistribution(env.DB, resolved, path[1], path[2]) }, } satisfies ExportedHandler