enterprise (#4617)
Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opentui/core": "0.1.47",
|
||||
"@opentui/solid": "0.1.47",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
@@ -70,7 +71,7 @@
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "1.1.1",
|
||||
"hono-openapi": "catalog:",
|
||||
"ignore": "7.0.5",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
} from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { Binary } from "@/util/binary"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { Snapshot } from "@/snapshot"
|
||||
import { useExit } from "./exit"
|
||||
|
||||
@@ -609,6 +609,11 @@ export namespace Config {
|
||||
})
|
||||
.optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
enterprise: z
|
||||
.object({
|
||||
url: z.string().optional().describe("Enterprise URL"),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
|
||||
@@ -10,11 +10,13 @@ import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
import { Log } from "@/util/log"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
ShareNext.init()
|
||||
Format.init()
|
||||
await LSP.init()
|
||||
FileWatcher.init()
|
||||
|
||||
@@ -42,6 +42,7 @@ import { Snapshot } from "@/snapshot"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -221,6 +222,15 @@ export namespace Session {
|
||||
throw new Error("Sharing is disabled in configuration")
|
||||
}
|
||||
|
||||
if (cfg.enterprise?.url) {
|
||||
const share = await ShareNext.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
url: share.url,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const session = await get(id)
|
||||
if (session.share) return session.share
|
||||
const share = await Share.create(id)
|
||||
@@ -241,6 +251,13 @@ export namespace Session {
|
||||
})
|
||||
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.enterprise?.url) {
|
||||
await ShareNext.remove(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
})
|
||||
}
|
||||
const share = await getShare(id)
|
||||
if (!share) return
|
||||
await Storage.remove(["share", id])
|
||||
|
||||
@@ -319,8 +319,6 @@ export namespace SessionProcessor {
|
||||
break
|
||||
|
||||
case "finish":
|
||||
input.assistantMessage.time.completed = Date.now()
|
||||
await Session.updateMessage(input.assistantMessage)
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
148
packages/opencode/src/share/share-next.ts
Normal file
148
packages/opencode/src/share/share-next.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { Session } from "@/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Log } from "@/util/log"
|
||||
import type * as SDK from "@opencode-ai/sdk"
|
||||
|
||||
export namespace ShareNext {
|
||||
const log = Log.create({ service: "share-next" })
|
||||
export async function init() {
|
||||
const config = await Config.get()
|
||||
if (!config.enterprise) return
|
||||
Bus.subscribe(Session.Event.Updated, async (evt) => {
|
||||
await sync(evt.properties.info.id, [
|
||||
{
|
||||
type: "session",
|
||||
data: evt.properties.info,
|
||||
},
|
||||
])
|
||||
})
|
||||
Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
|
||||
await sync(evt.properties.info.sessionID, [
|
||||
{
|
||||
type: "message",
|
||||
data: evt.properties.info,
|
||||
},
|
||||
])
|
||||
})
|
||||
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
||||
await sync(evt.properties.part.sessionID, [
|
||||
{
|
||||
type: "part",
|
||||
data: evt.properties.part,
|
||||
},
|
||||
])
|
||||
})
|
||||
Bus.subscribe(Session.Event.Diff, async (evt) => {
|
||||
await sync(evt.properties.sessionID, [
|
||||
{
|
||||
type: "session_diff",
|
||||
data: evt.properties.diff,
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
log.info("creating share", { sessionID })
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const result = await fetch(`${url}/api/share`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ sessionID: sessionID }),
|
||||
})
|
||||
.then((x) => x.json())
|
||||
.then((x) => x as { url: string; secret: string })
|
||||
await Storage.write(["session_share", sessionID], {
|
||||
id: sessionID,
|
||||
...result,
|
||||
})
|
||||
fullSync(sessionID)
|
||||
return result
|
||||
}
|
||||
|
||||
function get(sessionID: string) {
|
||||
return Storage.read<{
|
||||
id: string
|
||||
secret: string
|
||||
url: string
|
||||
}>(["session_share", sessionID])
|
||||
}
|
||||
|
||||
type Data =
|
||||
| {
|
||||
type: "session"
|
||||
data: SDK.Session
|
||||
}
|
||||
| {
|
||||
type: "message"
|
||||
data: SDK.Message
|
||||
}
|
||||
| {
|
||||
type: "part"
|
||||
data: SDK.Part
|
||||
}
|
||||
| {
|
||||
type: "session_diff"
|
||||
data: SDK.FileDiff[]
|
||||
}
|
||||
|
||||
async function sync(sessionID: string, data: Data[]) {
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const share = await get(sessionID)
|
||||
if (!share) return
|
||||
await fetch(`${url}/api/share/${share.id}/sync`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret: share.secret,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove(sessionID: string) {
|
||||
log.info("removing share", { sessionID })
|
||||
const url = await Config.get().then((x) => x.enterprise!.url)
|
||||
const share = await get(sessionID)
|
||||
if (!share) return
|
||||
await fetch(`${url}/api/share/${share.id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret: share.secret,
|
||||
}),
|
||||
})
|
||||
await Storage.remove(["session_share", share.id])
|
||||
}
|
||||
|
||||
async function fullSync(sessionID: string) {
|
||||
log.info("full sync", { sessionID })
|
||||
const session = await Session.get(sessionID)
|
||||
const diffs = await Session.diff(sessionID)
|
||||
const messages = await Array.fromAsync(MessageV2.stream(sessionID))
|
||||
await sync(sessionID, [
|
||||
{
|
||||
type: "session",
|
||||
data: session,
|
||||
},
|
||||
...messages.map((x) => ({
|
||||
type: "message" as const,
|
||||
data: x.info,
|
||||
})),
|
||||
...messages.flatMap((x) => x.parts.map((y) => ({ type: "part" as const, data: y }))),
|
||||
{
|
||||
type: "session_diff",
|
||||
data: diffs,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
export namespace Binary {
|
||||
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
|
||||
let left = 0
|
||||
let right = array.length - 1
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midId = compare(array[mid])
|
||||
|
||||
if (midId === id) {
|
||||
return { found: true, index: mid }
|
||||
} else if (midId < id) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false, index: left }
|
||||
}
|
||||
|
||||
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
|
||||
const id = compare(item)
|
||||
let left = 0
|
||||
let right = array.length
|
||||
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midId = compare(array[mid])
|
||||
|
||||
if (midId < id) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid
|
||||
}
|
||||
}
|
||||
|
||||
array.splice(left, 0, item)
|
||||
return array
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user