platform-node

This commit is contained in:
Brendan Allan
2026-05-04 17:02:43 +08:00
parent 41cf0f36bd
commit 36d8973925
4 changed files with 76 additions and 74 deletions

View File

@@ -265,6 +265,7 @@
"name": "@opencode-ai/desktop-electron",
"version": "1.14.34",
"dependencies": {
"@effect/platform-node": "catalog:",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron-context-menu": "4.1.2",

View File

@@ -25,6 +25,7 @@
"main": "./out/main/index.js",
"dependencies": {
"effect": "catalog:",
"@effect/platform-node": "catalog:",
"electron-context-menu": "4.1.2",
"electron-log": "^5",
"electron-store": "^10",

View File

@@ -4,6 +4,8 @@ import { createServer } from "node:net"
import { homedir } from "node:os"
import { join } from "node:path"
import { app, BrowserWindow, dialog } from "electron"
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import pkg from "electron-updater"
import { Data, Deferred, Effect, Fiber, Option, PubSub, Queue, Ref, Stream, SubscriptionRef } from "effect"
@@ -312,7 +314,7 @@ const main = Effect.gen(function* () {
),
)
void Effect.runPromise(main)
main.pipe(Effect.provide(NodeHttpClient.layerFetch), NodeRuntime.runMain())
const wireMenu = (win: BrowserWindow) => {
createMenu({
@@ -364,10 +366,10 @@ const registerIpcHandlersImpl = () =>
Effect.tap(() => Deferred.succeed(deepLinksConsumed, undefined)),
),
),
getDefaultServerUrl: () => Effect.runPromise(getDefaultServerUrl),
setDefaultServerUrl: (url) => Effect.runPromise(setDefaultServerUrl(url)),
getWslConfig: () => Effect.runPromise(getWslConfig),
setWslConfig: (config: WslConfig) => Effect.runPromise(setWslConfig(config)),
getDefaultServerUrl: () => getDefaultServerUrl(),
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
getWslConfig: () => Promise.resolve(getWslConfig()),
setWslConfig: (config: WslConfig) => setWslConfig(config),
getDisplayBackend: () => Promise.resolve(null),
setDisplayBackend: () => Promise.resolve(undefined),
parseMarkdown: (markdown) => Promise.resolve(parseMarkdown(markdown)),

View File

@@ -1,81 +1,83 @@
import { app } from "electron"
import { Effect } from "effect"
import { Effect, Option } from "effect"
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
import { getUserShell, loadShellEnv } from "./shell-env"
import { getStore } from "./store"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
export type WslConfig = { enabled: boolean }
export const getDefaultServerUrl = Effect.sync((): string | null => {
export const getDefaultServerUrl = (): string | null => {
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
return typeof value === "string" ? value : null
})
}
export const setDefaultServerUrl = (url: string | null) =>
Effect.sync(() => {
if (url) {
getStore().set(DEFAULT_SERVER_URL_KEY, url)
return
}
getStore().delete(DEFAULT_SERVER_URL_KEY)
})
export const setDefaultServerUrl = (url: string | null) => {
if (url) {
getStore().set(DEFAULT_SERVER_URL_KEY, url)
return
}
getStore().delete(DEFAULT_SERVER_URL_KEY)
}
export const getWslConfig = Effect.sync((): WslConfig => {
export const getWslConfig = (): WslConfig => {
const value = getStore().get(WSL_ENABLED_KEY)
return { enabled: typeof value === "boolean" ? value : false }
})
}
export const setWslConfig = (config: WslConfig) =>
Effect.sync(() => getStore().set(WSL_ENABLED_KEY, config.enabled))
export const setWslConfig = (config: WslConfig) => getStore().set(WSL_ENABLED_KEY, config.enabled)
export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")(
function* (hostname: string, port: number, password: string) {
yield* prepareServerEnv(password)
const { Log, Server } = yield* Effect.promise(() =>
import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
)
yield* Effect.promise(() => Log.init({ level: "WARN" }))
const listener = yield* Effect.promise(() =>
Server.listen({
port,
hostname,
username: "opencode",
password,
cors: ["oc://renderer"],
}),
)
export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")(function* (
hostname: string,
port: number,
password: string,
) {
prepareServerEnv(password)
const { Log, Server } = yield* Effect.promise(
() => import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
)
yield* Effect.promise(() => Log.init({ level: "WARN" }))
const listener = yield* Effect.promise(() =>
Server.listen({
port,
hostname,
username: "opencode",
password,
cors: ["oc://renderer"],
}),
)
const healthCheck = Effect.gen(function* () {
const url = `http://${hostname}:${port}`
while (true) {
const healthy = yield* checkHealthEffect(url, password)
if (healthy) return
yield* Effect.sleep("100 millis")
}
})
return { listener, health: healthCheck }
},
)
const prepareServerEnv = (password: string) =>
Effect.sync(() => {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
const env = {
...process.env,
...shellEnv,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: app.getPath("userData"),
const healthCheck = Effect.gen(function* () {
const url = `http://${hostname}:${port}`
while (true) {
const healthy = yield* checkHealthEffect(url, password)
if (healthy) return
yield* Effect.sleep("100 millis")
}
Object.assign(process.env, env)
})
return { listener, health: healthCheck }
})
const prepareServerEnv = (password: string) => () => {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
const env = {
...process.env,
...shellEnv,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: app.getPath("userData"),
}
Object.assign(process.env, env)
}
export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url: string, password?: string | null) {
const httpClient = yield* HttpClient.HttpClient
let healthUrl: URL
try {
healthUrl = new URL("/global/health", url)
@@ -89,16 +91,12 @@ export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url:
headers.set("authorization", `Basic ${auth}`)
}
try {
const res = yield* Effect.promise(() =>
fetch(healthUrl, {
method: "GET",
headers,
signal: AbortSignal.timeout(3000),
}),
return yield* httpClient
.get(healthUrl, { headers })
.pipe(
Effect.timeout("3 seconds"),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.option,
Effect.map(Option.isSome),
)
return res.ok
} catch {
return false
}
})