mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-17 12:58:34 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61742bc045 |
@@ -10,17 +10,15 @@ import { Updater } from "../../services/updater"
|
||||
import { UpdatePreflight } from "../../services/update-preflight"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { OPENCODE_CHANNEL, OPENCODE_VERSION } from "../../version"
|
||||
import { Env } from "../../env"
|
||||
|
||||
export default Runtime.handler(Commands, (input) =>
|
||||
Effect.gen(function* () {
|
||||
const requestedDirectory = Option.getOrUndefined(input.directory)
|
||||
const requestedServer = Option.getOrUndefined(input.server)
|
||||
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
|
||||
const preflight = UpdatePreflight.make()
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: requestedServer,
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
mismatch: "replace",
|
||||
onStart: (reason, previousVersion) => {
|
||||
@@ -77,7 +75,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
resolve: (spec) =>
|
||||
runPromise(npm.add(spec, { subpaths: ["tui"] }).pipe(Effect.map((result) => result.entrypoint))),
|
||||
},
|
||||
environment: requestedServer === undefined ? Env.session() : undefined,
|
||||
terminalHandoff: () => preflight.finish(),
|
||||
log: (level, message, tags) => {
|
||||
const effect =
|
||||
|
||||
@@ -12,13 +12,4 @@ export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
|
||||
Config.withDefault(undefined),
|
||||
)
|
||||
|
||||
export function session() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
(entry): entry is [string, string] =>
|
||||
entry[1] !== undefined && entry[0] !== "OPENCODE_PASSWORD" && entry[0] !== "OPENCODE_SERVER_PASSWORD",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export * as Env from "./env"
|
||||
|
||||
@@ -5,7 +5,6 @@ import { setTimeout } from "node:timers/promises"
|
||||
import { readStdin } from "./util/io"
|
||||
import { createMiniHost, INTERACTIVE_INPUT_ERROR, usingInteractiveStdin } from "./mini-host"
|
||||
import { parseSessionTargetModel, resolveSessionTarget, type SessionTargetPreparation } from "./session-target"
|
||||
import { Env } from "./env"
|
||||
|
||||
export type MiniCommandInput = {
|
||||
server: {
|
||||
@@ -39,7 +38,6 @@ export async function runMini(input: MiniCommandInput) {
|
||||
const directory = localDirectory()
|
||||
const connection = createMiniConnection(input.server)
|
||||
const sdk = connection.sdk
|
||||
const environment = input.server.reconnect ? Env.session() : undefined
|
||||
const requested = parseModel(input.model)
|
||||
const model = requested ? { providerID: requested.providerID, modelID: requested.id } : undefined
|
||||
const prepare = prepareTarget(input.agent)
|
||||
@@ -57,7 +55,6 @@ export async function runMini(input: MiniCommandInput) {
|
||||
fork: input.fork,
|
||||
model: requested,
|
||||
agent: input.agent,
|
||||
environment,
|
||||
prepare,
|
||||
signal,
|
||||
}).catch((error) => {
|
||||
@@ -92,7 +89,6 @@ export async function runMini(input: MiniCommandInput) {
|
||||
client,
|
||||
location: { directory: next.location.directory, workspace: next.location.workspaceID },
|
||||
agent: next.agent,
|
||||
environment,
|
||||
model: next.model
|
||||
? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant }
|
||||
: undefined,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { parseSessionTargetModel, resolveSessionTarget } from "../session-target
|
||||
import { toolInlineInfo } from "@opencode-ai/tui/mini/tool"
|
||||
import { runNonInteractivePrompt } from "./noninteractive"
|
||||
import { UI } from "./ui"
|
||||
import { Env } from "../env"
|
||||
|
||||
export type RunCommandInput = {
|
||||
server: ServerConnection.Resolved
|
||||
@@ -92,7 +91,6 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
|
||||
? { providerID: explicit.model.providerID, id: explicit.model.modelID, variant: explicit.variant }
|
||||
: undefined,
|
||||
agent: input.agent,
|
||||
environment: input.server.service ? Env.session() : undefined,
|
||||
prepare: async (next) => {
|
||||
const selected =
|
||||
next.model ??
|
||||
|
||||
@@ -36,7 +36,6 @@ export async function resolveSessionTarget(input: {
|
||||
fork?: boolean
|
||||
model?: ModelRef
|
||||
agent?: string
|
||||
environment?: Readonly<Record<string, string>>
|
||||
prepare: SessionTargetPreparation
|
||||
signal?: AbortSignal
|
||||
}): Promise<SessionTarget> {
|
||||
@@ -71,12 +70,6 @@ export async function resolveSessionTarget(input: {
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
}))
|
||||
if (input.environment !== undefined && location.workspaceID === undefined)
|
||||
await input.client.session
|
||||
.environment({ sessionID: session.id, variables: input.environment }, ...requestOptions(input.signal))
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
return {
|
||||
session,
|
||||
location,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Env } from "../src/env"
|
||||
|
||||
test("session environment omits server credentials", () => {
|
||||
const previousPassword = process.env.OPENCODE_PASSWORD
|
||||
const previousLegacyPassword = process.env.OPENCODE_SERVER_PASSWORD
|
||||
const previousValue = process.env.OPENCODE_SESSION_ENV_TEST
|
||||
process.env.OPENCODE_PASSWORD = "password"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "legacy"
|
||||
process.env.OPENCODE_SESSION_ENV_TEST = "included"
|
||||
|
||||
const environment = Env.session()
|
||||
|
||||
if (previousPassword === undefined) delete process.env.OPENCODE_PASSWORD
|
||||
else process.env.OPENCODE_PASSWORD = previousPassword
|
||||
if (previousLegacyPassword === undefined) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||
else process.env.OPENCODE_SERVER_PASSWORD = previousLegacyPassword
|
||||
if (previousValue === undefined) delete process.env.OPENCODE_SESSION_ENV_TEST
|
||||
else process.env.OPENCODE_SESSION_ENV_TEST = previousValue
|
||||
|
||||
expect(environment.OPENCODE_PASSWORD).toBeUndefined()
|
||||
expect(environment.OPENCODE_SERVER_PASSWORD).toBeUndefined()
|
||||
expect(environment.OPENCODE_SESSION_ENV_TEST).toBe("included")
|
||||
})
|
||||
@@ -55,26 +55,6 @@ describe("session target resolver", () => {
|
||||
expect(target.session.id).toBe("ses_implicit")
|
||||
})
|
||||
|
||||
test("attaches the terminal environment to the resolved local Session", async () => {
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const selected = session("ses_resume", "/session")
|
||||
spyOn(client.session, "get").mockResolvedValue(selected)
|
||||
spyOn(client.location, "get").mockResolvedValue(location("/session"))
|
||||
const environment = spyOn(client.session, "environment").mockResolvedValue()
|
||||
|
||||
await resolveSessionTarget({
|
||||
client,
|
||||
session: selected.id,
|
||||
environment: { PATH: "/terminal/bin" },
|
||||
prepare,
|
||||
})
|
||||
|
||||
expect(environment).toHaveBeenCalledWith({
|
||||
sessionID: selected.id,
|
||||
variables: { PATH: "/terminal/bin" },
|
||||
})
|
||||
})
|
||||
|
||||
test("prepares a fresh Session at the server Location before creation", async () => {
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const order: string[] = []
|
||||
|
||||
@@ -914,10 +914,6 @@ export type Endpoint5_34Input = { readonly sessionID: Session.ID; readonly messa
|
||||
export type Endpoint5_34Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_34Input) => Effect.Effect<Endpoint5_34Output, E>
|
||||
|
||||
export type Endpoint5_35Input = { readonly sessionID: Session.ID; readonly variables: { readonly [x: string]: string } }
|
||||
export type Endpoint5_35Output = void
|
||||
export type SessionEnvironmentOperation<E = never> = (input: Endpoint5_35Input) => Effect.Effect<Endpoint5_35Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
readonly create: SessionCreateOperation<E>
|
||||
@@ -962,7 +958,6 @@ export interface SessionApi<E = never> {
|
||||
readonly interrupt: SessionInterruptOperation<E>
|
||||
readonly background: SessionBackgroundOperation<E>
|
||||
readonly message: SessionMessageOperation<E>
|
||||
readonly environment: SessionEnvironmentOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint6_0Input = {
|
||||
|
||||
@@ -86,8 +86,6 @@ import type {
|
||||
Endpoint5_33Output,
|
||||
Endpoint5_34Input,
|
||||
Endpoint5_34Output,
|
||||
Endpoint5_35Input,
|
||||
Endpoint5_35Output,
|
||||
Endpoint6_0Input,
|
||||
Endpoint6_0Output,
|
||||
Endpoint7_0Input,
|
||||
@@ -612,14 +610,6 @@ const Endpoint5_34 = (raw: RawClient["server.session"]) => (input: Endpoint5_34I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_35 = (raw: RawClient["server.session"]) => (input: Endpoint5_35Input) =>
|
||||
preserveEffect<Endpoint5_35Output>()(
|
||||
raw["session.environment"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { variables: input["variables"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
list: Endpoint5_0(raw),
|
||||
create: Endpoint5_1(raw),
|
||||
@@ -649,7 +639,6 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
interrupt: Endpoint5_32(raw),
|
||||
background: Endpoint5_33(raw),
|
||||
message: Endpoint5_34(raw),
|
||||
environment: Endpoint5_35(raw),
|
||||
})
|
||||
|
||||
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
||||
|
||||
@@ -80,8 +80,6 @@ import type {
|
||||
SessionBackgroundOutput,
|
||||
SessionMessageInput,
|
||||
SessionMessageOutput,
|
||||
SessionEnvironmentInput,
|
||||
SessionEnvironmentOutput,
|
||||
MessageListInput,
|
||||
MessageListOutput,
|
||||
ModelListInput,
|
||||
@@ -898,18 +896,6 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
environment: (input: SessionEnvironmentInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionEnvironmentOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/environment`,
|
||||
body: { variables: input["variables"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
message: {
|
||||
list: (input: MessageListInput, requestOptions?: RequestOptions) =>
|
||||
|
||||
@@ -3936,13 +3936,6 @@ export type SessionMessageInput = {
|
||||
|
||||
export type SessionMessageOutput = { data: SessionMessageInfo }["data"]
|
||||
|
||||
export type SessionEnvironmentInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly variables: { readonly variables: { readonly [x: string]: string } }["variables"]
|
||||
}
|
||||
|
||||
export type SessionEnvironmentOutput = void
|
||||
|
||||
export type MessageListInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly limit?: {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import z from "zod"
|
||||
|
||||
const Body = z.object({
|
||||
workspaceID: z.string().startsWith("wrk_"),
|
||||
referralID: z.string().startsWith("ref_"),
|
||||
})
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = Body.safeParse(await event.request.json().catch(() => undefined))
|
||||
if (!body.success) {
|
||||
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
|
||||
}
|
||||
return Referral.restoreReward(body.data)
|
||||
.then((result) => Response.json({ success: true, message: "Referral reward restored", result }))
|
||||
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
|
||||
}
|
||||
@@ -475,6 +475,38 @@ export namespace Referral {
|
||||
})
|
||||
}
|
||||
|
||||
export async function restoreReward(input: { workspaceID: string; referralID: string }) {
|
||||
return Database.transaction(async (tx) => {
|
||||
const reward = await tx
|
||||
.select({ timeApplied: ReferralRewardTable.timeApplied })
|
||||
.from(ReferralRewardTable)
|
||||
.innerJoin(ReferralTable, eq(ReferralTable.id, ReferralRewardTable.referralID))
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, input.workspaceID),
|
||||
eq(ReferralRewardTable.referralID, input.referralID),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
isNull(ReferralTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!reward) throw new Error("Referral reward not found")
|
||||
|
||||
await tx
|
||||
.update(ReferralRewardTable)
|
||||
.set({ timeApplied: null })
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, input.workspaceID),
|
||||
eq(ReferralRewardTable.referralID, input.referralID),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
|
||||
return { restored: reward.timeApplied !== null }
|
||||
})
|
||||
}
|
||||
|
||||
export async function completeFromLiteSubscription(input: { workspaceID: string; userID: string }) {
|
||||
return Database.transaction(async (tx) => {
|
||||
const invitee = await tx
|
||||
|
||||
@@ -51,7 +51,6 @@ import { Global } from "@opencode-ai/util/global"
|
||||
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { fileURLToPath } from "url"
|
||||
import { SessionEnvironment } from "./session/environment.js"
|
||||
|
||||
// get project -> project.locations
|
||||
//
|
||||
@@ -166,10 +165,6 @@ export interface Interface {
|
||||
input: ForkInput,
|
||||
) => Effect.Effect<SessionSchema.Info, NotFoundError | MessageNotFoundError | ForkEmptyError>
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info, NotFoundError>
|
||||
readonly environment: (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly variables?: SessionEnvironment.Variables
|
||||
}) => Effect.Effect<SessionEnvironment.Variables | undefined, NotFoundError>
|
||||
readonly remove: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly messages: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -309,7 +304,6 @@ const layer = Layer.effect(
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const jobs = yield* Job.Service
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const activeShells = new Set<SessionSchema.ID>()
|
||||
const shellLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
@@ -453,11 +447,6 @@ const layer = Layer.effect(
|
||||
if (!session) return yield* new NotFoundError({ sessionID })
|
||||
return session
|
||||
}),
|
||||
environment: Effect.fn("Session.environment")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
if (input.variables !== undefined) yield* environments.set(input.sessionID, input.variables)
|
||||
return yield* environments.get(input.sessionID)
|
||||
}),
|
||||
remove: Effect.fn("Session.remove")(function* (sessionID) {
|
||||
const session = yield* result.get(sessionID)
|
||||
yield* execution.interrupt(sessionID)
|
||||
@@ -465,7 +454,6 @@ const layer = Layer.effect(
|
||||
yield* closeTransport(session)
|
||||
const children = yield* result.list({ parentID: sessionID })
|
||||
yield* Effect.forEach(children.data, (child) => result.remove(child.id), { concurrency: 1, discard: true })
|
||||
yield* environments.clear(sessionID)
|
||||
yield* bus.publish(SessionEvent.Deleted, { sessionID })
|
||||
yield* bus.remove(sessionID)
|
||||
}),
|
||||
@@ -664,12 +652,7 @@ const layer = Layer.effect(
|
||||
const started = yield* Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
return yield* shell
|
||||
.create({
|
||||
command: input.command,
|
||||
cwd: session.location.directory,
|
||||
timeout: 0,
|
||||
metadata: { sessionID: input.sessionID },
|
||||
})
|
||||
.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
|
||||
.pipe(Effect.orDie)
|
||||
}).pipe(Effect.provide(locations.get(session.location)))
|
||||
yield* bus.publish(
|
||||
@@ -1119,7 +1102,6 @@ export const node = makeGlobalNode({
|
||||
layer: layer.pipe(Layer.orDie),
|
||||
deps: [
|
||||
Job.node,
|
||||
SessionEnvironment.node,
|
||||
Database.node,
|
||||
Bus.node,
|
||||
Project.node,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
export * as SessionEnvironment from "./environment.js"
|
||||
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
|
||||
export type Variables = Readonly<Record<string, string>>
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<Variables | undefined>
|
||||
readonly set: (sessionID: SessionSchema.ID, variables: Variables) => Effect.Effect<void>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionEnvironment") {}
|
||||
|
||||
const layer = Layer.sync(Service, () => {
|
||||
const environments = new Map<SessionSchema.ID, Variables>()
|
||||
|
||||
return Service.of({
|
||||
get: (sessionID) => Effect.sync(() => environments.get(sessionID)),
|
||||
set: (sessionID, variables) =>
|
||||
Effect.sync(() => {
|
||||
environments.set(sessionID, { ...variables })
|
||||
}),
|
||||
clear: (sessionID) =>
|
||||
Effect.sync(() => {
|
||||
environments.delete(sessionID)
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||
@@ -15,8 +15,6 @@ import { Global } from "@opencode-ai/util/global"
|
||||
import { ShellSelect } from "./shell/select.js"
|
||||
import type { ShellCreateBefore } from "@opencode-ai/plugin/effect/shell"
|
||||
import { PluginHooks } from "./plugin/hooks.js"
|
||||
import { SessionEnvironment } from "./session/environment.js"
|
||||
import { SessionSchema } from "./session/schema.js"
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Shell.NotFoundError", {
|
||||
id: Shell.ID,
|
||||
@@ -78,7 +76,6 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
const global = yield* Global.Service
|
||||
const environment = yield* Environment.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const context = yield* Effect.context()
|
||||
const runFork = Effect.runForkWith(context)
|
||||
const sessions = new Map<string, Active>()
|
||||
@@ -187,18 +184,13 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
input: Shell.CreateInput,
|
||||
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
|
||||
) {
|
||||
const sessionID = input.metadata?.sessionID
|
||||
const sessionEnvironment =
|
||||
location.workspaceID === undefined && Schema.is(SessionSchema.ID)(sessionID)
|
||||
? yield* environments.get(sessionID)
|
||||
: undefined
|
||||
const invocation: ShellCreateBefore = {
|
||||
command: input.command,
|
||||
cwd: input.cwd ?? location.directory,
|
||||
timeout: input.timeout,
|
||||
shell: yield* resolve(),
|
||||
env: {
|
||||
...(sessionEnvironment ?? process.env),
|
||||
...process.env,
|
||||
TERM: "xterm-256color",
|
||||
OPENCODE_TERMINAL: "1",
|
||||
},
|
||||
@@ -357,15 +349,7 @@ export function configured(options?: ShellSelect.Options) {
|
||||
return makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer(options),
|
||||
deps: [
|
||||
Bus.node,
|
||||
Location.node,
|
||||
Config.node,
|
||||
Global.node,
|
||||
Environment.node,
|
||||
PluginHooks.node,
|
||||
SessionEnvironment.node,
|
||||
],
|
||||
deps: [Bus.node, Location.node, Config.node, Global.node, Environment.node, PluginHooks.node],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -679,18 +679,13 @@ describe("Session.create", () => {
|
||||
const created = yield* session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
yield* session.environment({ sessionID: created.id, variables: { OPENCODE_SESSION_ENV_TEST: "attached" } })
|
||||
|
||||
const command =
|
||||
process.platform === "win32"
|
||||
? "[Console]::Out.Write($env:OPENCODE_SESSION_ENV_TEST)"
|
||||
: 'printf %s "$OPENCODE_SESSION_ENV_TEST"'
|
||||
yield* session.shell({ sessionID: created.id, command })
|
||||
yield* session.shell({ sessionID: created.id, command: "echo hello" })
|
||||
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
|
||||
expect(shell).toMatchObject({ type: "shell", command, status: "exited", exit: 0 })
|
||||
expect(shell?.output?.output).toContain("attached")
|
||||
expect(shell).toMatchObject({ type: "shell", command: "echo hello", status: "exited", exit: 0 })
|
||||
expect(shell?.output?.output).toContain("hello")
|
||||
expect(shell?.output?.truncated).toBe(false)
|
||||
expect(shell?.time.completed).toBeDefined()
|
||||
}),
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionEnvironment } from "@opencode-ai/core/session/environment"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(SessionEnvironment.node))
|
||||
|
||||
describe("SessionEnvironment", () => {
|
||||
it.effect("stores replacement snapshots by session", () =>
|
||||
Effect.gen(function* () {
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const first = Session.ID.make("ses_environment_first")
|
||||
const second = Session.ID.make("ses_environment_second")
|
||||
|
||||
yield* environments.set(first, { TOOLCHAIN: "first", PATH: "/first/bin" })
|
||||
yield* environments.set(second, { TOOLCHAIN: "second" })
|
||||
yield* environments.set(first, { TOOLCHAIN: "updated" })
|
||||
|
||||
expect(yield* environments.get(first)).toEqual({ TOOLCHAIN: "updated" })
|
||||
expect(yield* environments.get(second)).toEqual({ TOOLCHAIN: "second" })
|
||||
|
||||
yield* environments.clear(first)
|
||||
|
||||
expect(yield* environments.get(first)).toBeUndefined()
|
||||
expect(yield* environments.get(second)).toEqual({ TOOLCHAIN: "second" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -12,7 +12,6 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionModelTransport } from "@opencode-ai/core/session/model-transport"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SessionEnvironment } from "@opencode-ai/core/session/environment"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { globalProjectLayer } from "./lib/project"
|
||||
@@ -33,7 +32,6 @@ const it = testEffect(
|
||||
Bus.node,
|
||||
SessionProjector.node,
|
||||
SessionStore.node,
|
||||
SessionEnvironment.node,
|
||||
Session.node,
|
||||
LocationServiceMap.node,
|
||||
]),
|
||||
@@ -52,8 +50,6 @@ describe("Session.remove", () => {
|
||||
const session = yield* Session.Service
|
||||
const parent = yield* session.create({ location })
|
||||
const child = yield* session.create({ parentID: parent.id })
|
||||
yield* session.environment({ sessionID: parent.id, variables: { SESSION_ENV: "parent" } })
|
||||
yield* session.environment({ sessionID: child.id, variables: { SESSION_ENV: "child" } })
|
||||
yield* (yield* LocationServiceMap.Service).contextEffect(location)
|
||||
closed.length = 0
|
||||
|
||||
@@ -61,9 +57,6 @@ describe("Session.remove", () => {
|
||||
|
||||
expect((yield* session.list()).data).toEqual([])
|
||||
expect(closed).toEqual([parent.id, child.id])
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
expect(yield* environments.get(parent.id)).toBeUndefined()
|
||||
expect(yield* environments.get(child.id)).toBeUndefined()
|
||||
expect(yield* Effect.result(session.get(parent.id))).toMatchObject({ _tag: "Failure" })
|
||||
expect(yield* Effect.result(session.get(child.id))).toMatchObject({ _tag: "Failure" })
|
||||
}),
|
||||
|
||||
@@ -260,36 +260,6 @@ describe("ShellTool", () => {
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
|
||||
productionIt.live(
|
||||
"uses the session environment instead of the server environment",
|
||||
() =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
yield* sessions.environment({
|
||||
sessionID,
|
||||
variables: { OPENCODE_SESSION_ENV_TEST: "from-session" },
|
||||
})
|
||||
const command = isWindows
|
||||
? "[Console]::Out.Write($env:OPENCODE_SESSION_ENV_TEST)"
|
||||
: 'printf %s "$OPENCODE_SESSION_ENV_TEST"'
|
||||
|
||||
const settled = yield* executeTool(registry, call({ command }))
|
||||
|
||||
expect(settled.status).toBe("completed")
|
||||
expect(settled.content?.[0]).toEqual({ type: "text", text: "from-session" })
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
|
||||
it.live("resolves a relative workdir from the active Location", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
@@ -693,20 +693,6 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("session.environment", "/api/session/:sessionID/environment", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ variables: Schema.Record(Schema.String, Schema.String) }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.environment",
|
||||
summary: "Set session environment",
|
||||
description: "Replace the process environment used by local shell commands for this session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "session",
|
||||
|
||||
@@ -196,22 +196,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.environment",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.environment({ sessionID: ctx.params.sessionID, variables: ctx.payload.variables }).pipe(
|
||||
Effect.catchTag(
|
||||
"Session.NotFoundError",
|
||||
(error) =>
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.fork",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
useTuiApp,
|
||||
useTuiPaths,
|
||||
useTuiStartup,
|
||||
useTuiTerminalEnvironment,
|
||||
type TuiApp,
|
||||
} from "./context/runtime"
|
||||
import { DialogProvider, useDialog } from "./ui/dialog"
|
||||
@@ -186,7 +185,6 @@ export type TuiInput = {
|
||||
args: Args
|
||||
config: Config.Interface
|
||||
packages: PackageResolver
|
||||
environment?: Readonly<Record<string, string>>
|
||||
terminalHandoff?: () => Promise<
|
||||
| {
|
||||
readonly renderer: CliRenderer
|
||||
@@ -334,7 +332,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
: process.env.DISPLAY
|
||||
? "x11"
|
||||
: undefined,
|
||||
variables: input.environment,
|
||||
}}
|
||||
>
|
||||
<TuiStartupProvider
|
||||
@@ -483,17 +480,6 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
const promptRef = usePromptRef()
|
||||
const plugins = usePlugin()
|
||||
const clipboard = useClipboard()
|
||||
const terminalEnvironment = useTuiTerminalEnvironment()
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") return
|
||||
if (route.data.type !== "session") return
|
||||
const session = data.session.get(route.data.sessionID)
|
||||
if (!session) return
|
||||
if (session.location.workspaceID !== undefined || terminalEnvironment.variables === undefined) return
|
||||
void client.api.session
|
||||
.environment({ sessionID: session.id, variables: terminalEnvironment.variables })
|
||||
.catch(toast.error)
|
||||
})
|
||||
const [layout, updateLayout] = useStorage().store<{ verticalTabsWidth?: number }>("layout", {
|
||||
initial: { verticalTabsWidth: SESSION_SIDEBAR_WIDTH },
|
||||
})
|
||||
|
||||
@@ -1206,19 +1206,6 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
sessionID = created.id
|
||||
session = created
|
||||
if (created.location.workspaceID === undefined && terminalEnvironment.variables !== undefined) {
|
||||
const error = await client.api.session
|
||||
.environment({ sessionID, variables: terminalEnvironment.variables })
|
||||
.then(
|
||||
() => undefined,
|
||||
(error) => error,
|
||||
)
|
||||
if (error) {
|
||||
if (finishMoveProgress) move.finishSubmit()
|
||||
toast.show({ title: "Failed to set session environment", message: errorMessage(error), variant: "error" })
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture mode before it gets reset
|
||||
|
||||
@@ -17,7 +17,6 @@ export type TuiTerminalEnvironment = Readonly<{
|
||||
platform: string
|
||||
multiplexer?: "tmux" | "screen"
|
||||
displayServer?: "wayland" | "x11"
|
||||
variables?: Readonly<Record<string, string>>
|
||||
}>
|
||||
|
||||
export type TuiStartup = Readonly<{
|
||||
|
||||
Reference in New Issue
Block a user