mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 16:26:14 -04:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad741cbea0 | |||
| cbc40a5981 | |||
| b9b210a864 | |||
| d473b7e971 | |||
| f5783c4313 | |||
| 9439a5647e | |||
| 2bfe81ee5c | |||
| fcf1bb010c | |||
| 08b6d9c6dc | |||
| 0293a8bb80 | |||
| 850dbb93eb | |||
| 48e867ee20 | |||
| b5ebc541b9 | |||
| bd7a4cec90 | |||
| 63af295a17 | |||
| 04954a9620 | |||
| fb63fd79a3 | |||
| 2e04b66eab | |||
| f0b7c8c374 | |||
| be6f59035a | |||
| 27ab51f490 | |||
| bca723e8fe | |||
| 1ac39718d8 | |||
| 190319fb56 | |||
| 3154f0a61c | |||
| 0b686b8178 | |||
| 4cba56171b | |||
| 66342acd31 | |||
| 88dae67549 | |||
| 0ec42582f3 | |||
| 4f82248a68 | |||
| 5e069aab97 | |||
| 5325b2ec99 | |||
| 2a98920922 | |||
| 5ea92ea6cb | |||
| a18528a7ee | |||
| ced125a974 | |||
| 655fe20beb | |||
| dd0c258e23 | |||
| 791e27d289 | |||
| fac0aec69f | |||
| ca26e639f6 | |||
| 0b5d54f2cb | |||
| 1b408cf06b | |||
| 8e102d19ed | |||
| 721b2406e9 | |||
| 4a6a18cd79 | |||
| c10b5880cc | |||
| e6bf83084c | |||
| 6722ee22ee | |||
| 870a5731ac | |||
| 7910ce5d36 | |||
| 6ad171dba9 | |||
| cb5674edc7 | |||
| b99de4118e | |||
| 040700dbc4 | |||
| 4d5da9697e | |||
| a28648f530 | |||
| 4d81e2d4d9 | |||
| 21e72cbf42 | |||
| 5f277d1e62 | |||
| d67e877e28 | |||
| d4e51e04b3 | |||
| 070c1679e4 | |||
| 406d216cd2 | |||
| 5dc8b4ef29 | |||
| 2f41d89163 | |||
| b2eae867a1 | |||
| 3c2fda4d91 | |||
| 2678ceb45e | |||
| 58a4cd00b6 | |||
| 0faa191b6d | |||
| 58cf092105 | |||
| 0ff8bfe1d9 | |||
| ceb79c786a | |||
| b1a15d559b | |||
| 124a8abf9b | |||
| 85c2bb342b | |||
| 4c57e39466 | |||
| 0cdd4e4e16 | |||
| a9b01be0c2 | |||
| 528daf5490 | |||
| 0e176d3ac3 | |||
| 27f359852e | |||
| 173128d431 | |||
| e8ee1e239f | |||
| 656fa191c1 |
@@ -128,7 +128,7 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
|||||||
|
|
||||||
#### How is this different from Claude Code?
|
#### How is this different from Claude Code?
|
||||||
|
|
||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences::
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||||
|
|||||||
@@ -89,8 +89,11 @@
|
|||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
|
"@hono/node-server": "1.19.11",
|
||||||
|
"@hono/node-ws": "1.3.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -104,7 +107,6 @@
|
|||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||||
"@opentui/core": "0.1.87",
|
"@opentui/core": "0.1.87",
|
||||||
"@opentui/solid": "0.1.87",
|
"@opentui/solid": "0.1.87",
|
||||||
"@effect/platform-node": "catalog:",
|
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const AcpCommand = cmd({
|
|||||||
process.env.OPENCODE_CLIENT = "acp"
|
process.env.OPENCODE_CLIENT = "acp"
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ServeCommand = cmd({
|
|||||||
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||||
|
|
||||||
await new Promise(() => {})
|
await new Promise(() => {})
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const WebCommand = cmd({
|
|||||||
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.logo(" "))
|
UI.println(UI.logo(" "))
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createAdaptorServer } from "@hono/node-server"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { InstanceBootstrap } from "../../project/bootstrap"
|
import { InstanceBootstrap } from "../../project/bootstrap"
|
||||||
@@ -56,10 +57,24 @@ export namespace WorkspaceServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Listen(opts: { hostname: string; port: number }) {
|
export function Listen(opts: { hostname: string; port: number }) {
|
||||||
return Bun.serve({
|
const server = createAdaptorServer({
|
||||||
hostname: opts.hostname,
|
|
||||||
port: opts.port,
|
|
||||||
fetch: App().fetch,
|
fetch: App().fetch,
|
||||||
})
|
})
|
||||||
|
server.listen(opts.port, opts.hostname)
|
||||||
|
return {
|
||||||
|
hostname: opts.hostname,
|
||||||
|
port: opts.port,
|
||||||
|
stop() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createConnection } from "net"
|
import { createConnection } from "net"
|
||||||
|
import { createServer } from "http"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
|
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
|
||||||
|
|
||||||
@@ -52,27 +53,18 @@ interface PendingAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace McpOAuthCallback {
|
export namespace McpOAuthCallback {
|
||||||
let server: ReturnType<typeof Bun.serve> | undefined
|
let server: ReturnType<typeof createServer> | undefined
|
||||||
const pendingAuths = new Map<string, PendingAuth>()
|
const pendingAuths = new Map<string, PendingAuth>()
|
||||||
|
|
||||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
||||||
|
|
||||||
export async function ensureRunning(): Promise<void> {
|
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
|
||||||
if (server) return
|
const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
|
||||||
|
|
||||||
const running = await isPortInUse()
|
|
||||||
if (running) {
|
|
||||||
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server = Bun.serve({
|
|
||||||
port: OAUTH_CALLBACK_PORT,
|
|
||||||
fetch(req) {
|
|
||||||
const url = new URL(req.url)
|
|
||||||
|
|
||||||
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
||||||
return new Response("Not found", { status: 404 })
|
res.writeHead(404)
|
||||||
|
res.end("Not found")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = url.searchParams.get("code")
|
const code = url.searchParams.get("code")
|
||||||
@@ -86,10 +78,9 @@ export namespace McpOAuthCallback {
|
|||||||
if (!state) {
|
if (!state) {
|
||||||
const errorMsg = "Missing required state parameter - potential CSRF attack"
|
const errorMsg = "Missing required state parameter - potential CSRF attack"
|
||||||
log.error("oauth callback missing state parameter", { url: url.toString() })
|
log.error("oauth callback missing state parameter", { url: url.toString() })
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(400, { "Content-Type": "text/html" })
|
||||||
status: 400,
|
res.end(HTML_ERROR(errorMsg))
|
||||||
headers: { "Content-Type": "text/html" },
|
return
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -100,26 +91,24 @@ export namespace McpOAuthCallback {
|
|||||||
pendingAuths.delete(state)
|
pendingAuths.delete(state)
|
||||||
pending.reject(new Error(errorMsg))
|
pending.reject(new Error(errorMsg))
|
||||||
}
|
}
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
headers: { "Content-Type": "text/html" },
|
res.end(HTML_ERROR(errorMsg))
|
||||||
})
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return new Response(HTML_ERROR("No authorization code provided"), {
|
res.writeHead(400, { "Content-Type": "text/html" })
|
||||||
status: 400,
|
res.end(HTML_ERROR("No authorization code provided"))
|
||||||
headers: { "Content-Type": "text/html" },
|
return
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate state parameter
|
// Validate state parameter
|
||||||
if (!pendingAuths.has(state)) {
|
if (!pendingAuths.has(state)) {
|
||||||
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
|
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
|
||||||
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(400, { "Content-Type": "text/html" })
|
||||||
status: 400,
|
res.end(HTML_ERROR(errorMsg))
|
||||||
headers: { "Content-Type": "text/html" },
|
return
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = pendingAuths.get(state)!
|
const pending = pendingAuths.get(state)!
|
||||||
@@ -128,13 +117,27 @@ export namespace McpOAuthCallback {
|
|||||||
pendingAuths.delete(state)
|
pendingAuths.delete(state)
|
||||||
pending.resolve(code)
|
pending.resolve(code)
|
||||||
|
|
||||||
return new Response(HTML_SUCCESS, {
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
headers: { "Content-Type": "text/html" },
|
res.end(HTML_SUCCESS)
|
||||||
})
|
}
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
export async function ensureRunning(): Promise<void> {
|
||||||
|
if (server) return
|
||||||
|
|
||||||
|
const running = await isPortInUse()
|
||||||
|
if (running) {
|
||||||
|
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server = createServer(handleRequest)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server!.listen(OAUTH_CALLBACK_PORT, () => {
|
||||||
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
server!.on("error", reject)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waitForCallback(oauthState: string): Promise<string> {
|
export function waitForCallback(oauthState: string): Promise<string> {
|
||||||
@@ -174,7 +177,7 @@ export namespace McpOAuthCallback {
|
|||||||
|
|
||||||
export async function stop(): Promise<void> {
|
export async function stop(): Promise<void> {
|
||||||
if (server) {
|
if (server) {
|
||||||
server.stop()
|
await new Promise<void>((resolve) => server!.close(() => resolve()))
|
||||||
server = undefined
|
server = undefined
|
||||||
log.info("oauth callback server stopped")
|
log.info("oauth callback server stopped")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// Workaround: Bun on Windows does not support the UV_FS_O_FILEMAP flag that
|
||||||
|
// the `tar` package uses for files < 512KB (fs.open returns EINVAL).
|
||||||
|
// tar silently swallows the error and skips writing files, leaving only empty
|
||||||
|
// directories. Setting __FAKE_PLATFORM__ makes tar fall back to the plain 'w'
|
||||||
|
// flag. See tar's get-write-flag.js.
|
||||||
|
// Must be set before @npmcli/arborist is imported since tar caches the flag
|
||||||
|
// at module evaluation time — so we use a dynamic import() below.
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
process.env.__FAKE_PLATFORM__ = "linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
import semver from "semver"
|
||||||
|
import z from "zod"
|
||||||
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import { Lock } from "../util/lock"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
|
import { readdir } from "fs/promises"
|
||||||
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
|
||||||
|
const { Arborist } = await import("@npmcli/arborist")
|
||||||
|
|
||||||
|
export namespace Npm {
|
||||||
|
const log = Log.create({ service: "npm" })
|
||||||
|
|
||||||
|
export const InstallFailedError = NamedError.create(
|
||||||
|
"NpmInstallFailedError",
|
||||||
|
z.object({
|
||||||
|
pkg: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function directory(pkg: string) {
|
||||||
|
return path.join(Global.Path.cache, "packages", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
|
||||||
|
const response = await fetch(`https://registry.npmjs.org/${pkg}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
|
||||||
|
const latestVersion = data?.["dist-tags"]?.latest
|
||||||
|
if (!latestVersion) {
|
||||||
|
log.warn("No latest version found, using cached", { pkg, cachedVersion })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
|
||||||
|
if (range) return !semver.satisfies(latestVersion, cachedVersion)
|
||||||
|
|
||||||
|
return semver.lt(cachedVersion, latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function add(pkg: string) {
|
||||||
|
using _ = await Lock.write(`npm-install:${pkg}`)
|
||||||
|
log.info("installing package", {
|
||||||
|
pkg,
|
||||||
|
})
|
||||||
|
const dir = directory(pkg)
|
||||||
|
|
||||||
|
const arborist = new Arborist({
|
||||||
|
path: dir,
|
||||||
|
binLinks: true,
|
||||||
|
progress: false,
|
||||||
|
savePrefix: "",
|
||||||
|
})
|
||||||
|
const tree = await arborist.loadVirtual().catch(() => {})
|
||||||
|
if (tree) {
|
||||||
|
const first = tree.edgesOut.values().next().value?.to
|
||||||
|
if (first) {
|
||||||
|
log.info("package already installed", { pkg })
|
||||||
|
return first.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await arborist
|
||||||
|
.reify({
|
||||||
|
add: [pkg],
|
||||||
|
save: true,
|
||||||
|
saveType: "prod",
|
||||||
|
})
|
||||||
|
.catch((cause) => {
|
||||||
|
throw new InstallFailedError(
|
||||||
|
{ pkg },
|
||||||
|
{
|
||||||
|
cause,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const first = result.edgesOut.values().next().value?.to
|
||||||
|
if (!first) throw new InstallFailedError({ pkg })
|
||||||
|
return first.path
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function install(dir: string) {
|
||||||
|
using _ = await Lock.write(`npm-install:${dir}`)
|
||||||
|
log.info("checking dependencies", { dir })
|
||||||
|
|
||||||
|
const reify = async () => {
|
||||||
|
const arb = new Arborist({
|
||||||
|
path: dir,
|
||||||
|
binLinks: true,
|
||||||
|
progress: false,
|
||||||
|
savePrefix: "",
|
||||||
|
})
|
||||||
|
await arb.reify().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
|
||||||
|
log.info("node_modules missing, reifying")
|
||||||
|
await reify()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
|
||||||
|
const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
|
||||||
|
|
||||||
|
const declared = new Set([
|
||||||
|
...Object.keys(pkg.dependencies || {}),
|
||||||
|
...Object.keys(pkg.devDependencies || {}),
|
||||||
|
...Object.keys(pkg.peerDependencies || {}),
|
||||||
|
...Object.keys(pkg.optionalDependencies || {}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const root = lock.packages?.[""] || {}
|
||||||
|
const locked = new Set([
|
||||||
|
...Object.keys(root.dependencies || {}),
|
||||||
|
...Object.keys(root.devDependencies || {}),
|
||||||
|
...Object.keys(root.peerDependencies || {}),
|
||||||
|
...Object.keys(root.optionalDependencies || {}),
|
||||||
|
])
|
||||||
|
|
||||||
|
for (const name of declared) {
|
||||||
|
if (!locked.has(name)) {
|
||||||
|
log.info("dependency not in lock file, reifying", { name })
|
||||||
|
await reify()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("dependencies in sync")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function which(pkg: string) {
|
||||||
|
const dir = directory(pkg)
|
||||||
|
const binDir = path.join(dir, "node_modules", ".bin")
|
||||||
|
|
||||||
|
const pick = async () => {
|
||||||
|
const files = await readdir(binDir).catch(() => [])
|
||||||
|
if (files.length === 0) return undefined
|
||||||
|
if (files.length === 1) return files[0]
|
||||||
|
// Multiple binaries — resolve from package.json bin field like npx does
|
||||||
|
const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
|
||||||
|
path.join(dir, "node_modules", pkg, "package.json"),
|
||||||
|
).catch(() => undefined)
|
||||||
|
if (pkgJson?.bin) {
|
||||||
|
const bin = pkgJson.bin
|
||||||
|
if (typeof bin === "string") return path.basename(bin)
|
||||||
|
const keys = Object.keys(bin)
|
||||||
|
if (keys.length === 1) return keys[0]
|
||||||
|
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
|
||||||
|
return bin[unscoped] ? unscoped : keys[0]
|
||||||
|
}
|
||||||
|
return files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const bin = await pick()
|
||||||
|
if (bin) return path.join(binDir, bin)
|
||||||
|
|
||||||
|
await add(pkg)
|
||||||
|
const resolved = await pick()
|
||||||
|
if (!resolved) throw new Error(`No binary found for package "${pkg}" after install`)
|
||||||
|
return path.join(binDir, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import os from "os"
|
|||||||
import { ProviderTransform } from "@/provider/transform"
|
import { ProviderTransform } from "@/provider/transform"
|
||||||
import { ModelID, ProviderID } from "@/provider/schema"
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
|
import { createServer } from "http"
|
||||||
|
|
||||||
const log = Log.create({ service: "plugin.codex" })
|
const log = Log.create({ service: "plugin.codex" })
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ interface PendingOAuth {
|
|||||||
reject: (error: Error) => void
|
reject: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let oauthServer: ReturnType<typeof Bun.serve> | undefined
|
let oauthServer: ReturnType<typeof createServer> | undefined
|
||||||
let pendingOAuth: PendingOAuth | undefined
|
let pendingOAuth: PendingOAuth | undefined
|
||||||
|
|
||||||
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
|
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
|
||||||
@@ -249,10 +250,8 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
|||||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthServer = Bun.serve({
|
oauthServer = createServer((req, res) => {
|
||||||
port: OAUTH_PORT,
|
const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`)
|
||||||
fetch(req) {
|
|
||||||
const url = new URL(req.url)
|
|
||||||
|
|
||||||
if (url.pathname === "/auth/callback") {
|
if (url.pathname === "/auth/callback") {
|
||||||
const code = url.searchParams.get("code")
|
const code = url.searchParams.get("code")
|
||||||
@@ -264,29 +263,27 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
|||||||
const errorMsg = errorDescription || error
|
const errorMsg = errorDescription || error
|
||||||
pendingOAuth?.reject(new Error(errorMsg))
|
pendingOAuth?.reject(new Error(errorMsg))
|
||||||
pendingOAuth = undefined
|
pendingOAuth = undefined
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
headers: { "Content-Type": "text/html" },
|
res.end(HTML_ERROR(errorMsg))
|
||||||
})
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const errorMsg = "Missing authorization code"
|
const errorMsg = "Missing authorization code"
|
||||||
pendingOAuth?.reject(new Error(errorMsg))
|
pendingOAuth?.reject(new Error(errorMsg))
|
||||||
pendingOAuth = undefined
|
pendingOAuth = undefined
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(400, { "Content-Type": "text/html" })
|
||||||
status: 400,
|
res.end(HTML_ERROR(errorMsg))
|
||||||
headers: { "Content-Type": "text/html" },
|
return
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingOAuth || state !== pendingOAuth.state) {
|
if (!pendingOAuth || state !== pendingOAuth.state) {
|
||||||
const errorMsg = "Invalid state - potential CSRF attack"
|
const errorMsg = "Invalid state - potential CSRF attack"
|
||||||
pendingOAuth?.reject(new Error(errorMsg))
|
pendingOAuth?.reject(new Error(errorMsg))
|
||||||
pendingOAuth = undefined
|
pendingOAuth = undefined
|
||||||
return new Response(HTML_ERROR(errorMsg), {
|
res.writeHead(400, { "Content-Type": "text/html" })
|
||||||
status: 400,
|
res.end(HTML_ERROR(errorMsg))
|
||||||
headers: { "Content-Type": "text/html" },
|
return
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = pendingOAuth
|
const current = pendingOAuth
|
||||||
@@ -296,30 +293,40 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
|||||||
.then((tokens) => current.resolve(tokens))
|
.then((tokens) => current.resolve(tokens))
|
||||||
.catch((err) => current.reject(err))
|
.catch((err) => current.reject(err))
|
||||||
|
|
||||||
return new Response(HTML_SUCCESS, {
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
headers: { "Content-Type": "text/html" },
|
res.end(HTML_SUCCESS)
|
||||||
})
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === "/cancel") {
|
if (url.pathname === "/cancel") {
|
||||||
pendingOAuth?.reject(new Error("Login cancelled"))
|
pendingOAuth?.reject(new Error("Login cancelled"))
|
||||||
pendingOAuth = undefined
|
pendingOAuth = undefined
|
||||||
return new Response("Login cancelled", { status: 200 })
|
res.writeHead(200)
|
||||||
|
res.end("Login cancelled")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response("Not found", { status: 404 })
|
res.writeHead(404)
|
||||||
},
|
res.end("Not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
oauthServer!.listen(OAUTH_PORT, () => {
|
||||||
log.info("codex oauth server started", { port: OAUTH_PORT })
|
log.info("codex oauth server started", { port: OAUTH_PORT })
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
oauthServer!.on("error", reject)
|
||||||
|
})
|
||||||
|
|
||||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopOAuthServer() {
|
function stopOAuthServer() {
|
||||||
if (oauthServer) {
|
if (oauthServer) {
|
||||||
oauthServer.stop()
|
oauthServer.close(() => {
|
||||||
oauthServer = undefined
|
|
||||||
log.info("codex oauth server stopped")
|
log.info("codex oauth server stopped")
|
||||||
|
})
|
||||||
|
oauthServer = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export namespace Pty {
|
|||||||
close: (code?: number, reason?: string) => void
|
close: (code?: number, reason?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
|
||||||
|
|
||||||
// WebSocket control frame: 0x00 + UTF-8 JSON.
|
// WebSocket control frame: 0x00 + UTF-8 JSON.
|
||||||
const meta = (cursor: number) => {
|
const meta = (cursor: number) => {
|
||||||
const json = JSON.stringify({ cursor })
|
const json = JSON.stringify({ cursor })
|
||||||
@@ -97,9 +99,9 @@ export namespace Pty {
|
|||||||
try {
|
try {
|
||||||
session.process.kill()
|
session.process.kill()
|
||||||
} catch {}
|
} catch {}
|
||||||
for (const [key, ws] of session.subscribers.entries()) {
|
for (const [id, ws] of session.subscribers.entries()) {
|
||||||
try {
|
try {
|
||||||
if (ws.data === key) ws.close()
|
if (key(ws) === id) ws.close()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -230,9 +232,9 @@ export namespace Pty {
|
|||||||
try {
|
try {
|
||||||
session.process.kill()
|
session.process.kill()
|
||||||
} catch {}
|
} catch {}
|
||||||
for (const [key, ws] of session.subscribers.entries()) {
|
for (const [id, ws] of session.subscribers.entries()) {
|
||||||
try {
|
try {
|
||||||
if (ws.data === key) ws.close()
|
if (key(ws) === id) ws.close()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -263,16 +265,13 @@ export namespace Pty {
|
|||||||
}
|
}
|
||||||
log.info("client connected to session", { id })
|
log.info("client connected to session", { id })
|
||||||
|
|
||||||
// Use ws.data as the unique key for this connection lifecycle.
|
const sub = key(ws)
|
||||||
// If ws.data is undefined, fallback to ws object.
|
|
||||||
const connectionKey = ws.data && typeof ws.data === "object" ? ws.data : ws
|
|
||||||
|
|
||||||
// Optionally cleanup if the key somehow exists
|
session.subscribers.delete(sub)
|
||||||
session.subscribers.delete(connectionKey)
|
session.subscribers.set(sub, ws)
|
||||||
session.subscribers.set(connectionKey, ws)
|
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
session.subscribers.delete(connectionKey)
|
session.subscribers.delete(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = session.bufferCursor
|
const start = session.bufferCursor
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const ProjectRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const projects = await Project.list()
|
const projects = Project.list()
|
||||||
return c.json(projects)
|
return c.json(projects)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
import { upgradeWebSocket } from "hono/bun"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { PtyID } from "@/pty/schema"
|
import { PtyID } from "@/pty/schema"
|
||||||
import { NotFoundError } from "../../storage/db"
|
import { NotFoundError } from "../../storage/db"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
|
||||||
|
|
||||||
export const PtyRoutes = lazy(() =>
|
export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||||
new Hono()
|
return new Hono()
|
||||||
.get(
|
.get(
|
||||||
"/",
|
"/",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -197,5 +196,5 @@ export const PtyRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { streamSSE } from "hono/streaming"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { BusEvent } from "../bus/bus-event"
|
||||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
@@ -25,7 +28,7 @@ import { ProviderID } from "../provider/schema"
|
|||||||
import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
|
import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
|
||||||
import { ProjectRoutes } from "./routes/project"
|
import { ProjectRoutes } from "./routes/project"
|
||||||
import { SessionRoutes } from "./routes/session"
|
import { SessionRoutes } from "./routes/session"
|
||||||
import { PtyRoutes } from "./routes/pty"
|
// import { PtyRoutes } from "./routes/pty"
|
||||||
import { McpRoutes } from "./routes/mcp"
|
import { McpRoutes } from "./routes/mcp"
|
||||||
import { FileRoutes } from "./routes/file"
|
import { FileRoutes } from "./routes/file"
|
||||||
import { ConfigRoutes } from "./routes/config"
|
import { ConfigRoutes } from "./routes/config"
|
||||||
@@ -35,7 +38,8 @@ import { EventRoutes } from "./routes/event"
|
|||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { NotFoundError } from "../storage/db"
|
import { NotFoundError } from "../storage/db"
|
||||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||||
import { websocket } from "hono/bun"
|
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
||||||
|
import { createNodeWebSocket } from "@hono/node-ws"
|
||||||
import { HTTPException } from "hono/http-exception"
|
import { HTTPException } from "hono/http-exception"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
@@ -49,13 +53,20 @@ import { lazy } from "@/util/lazy"
|
|||||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
|
|
||||||
export namespace Server {
|
export namespace Server {
|
||||||
|
export type Listener = {
|
||||||
|
hostname: string
|
||||||
|
port: number
|
||||||
|
url: URL
|
||||||
|
stop: (close?: boolean) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default = lazy(() => create({}).app)
|
||||||
|
|
||||||
|
function create(opts: { cors?: string[] }) {
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
export const Default = lazy(() => createApp({}))
|
|
||||||
|
|
||||||
export const createApp = (opts: { cors?: string[] }): Hono => {
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
return app
|
const ws = createNodeWebSocket({ app })
|
||||||
|
const route = app
|
||||||
.onError((err, c) => {
|
.onError((err, c) => {
|
||||||
log.error("failed", {
|
log.error("failed", {
|
||||||
error: err,
|
error: err,
|
||||||
@@ -241,7 +252,6 @@ export namespace Server {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.route("/project", ProjectRoutes())
|
.route("/project", ProjectRoutes())
|
||||||
.route("/pty", PtyRoutes())
|
|
||||||
.route("/config", ConfigRoutes())
|
.route("/config", ConfigRoutes())
|
||||||
.route("/experimental", ExperimentalRoutes())
|
.route("/experimental", ExperimentalRoutes())
|
||||||
.route("/session", SessionRoutes())
|
.route("/session", SessionRoutes())
|
||||||
@@ -497,22 +507,70 @@ export namespace Server {
|
|||||||
return c.json(await Format.status())
|
return c.json(await Format.status())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.all("/*", async (c) => {
|
.get(
|
||||||
const path = c.req.path
|
"/event",
|
||||||
|
describeRoute({
|
||||||
const response = await proxy(`https://app.opencode.ai${path}`, {
|
summary: "Subscribe to events",
|
||||||
...c.req,
|
description: "Get events",
|
||||||
headers: {
|
operationId: "event.subscribe",
|
||||||
...c.req.raw.headers,
|
responses: {
|
||||||
host: "app.opencode.ai",
|
200: {
|
||||||
|
description: "Event stream",
|
||||||
|
content: {
|
||||||
|
"text/event-stream": {
|
||||||
|
schema: resolver(BusEvent.payloads()),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
log.info("event connected")
|
||||||
|
c.header("X-Accel-Buffering", "no")
|
||||||
|
c.header("X-Content-Type-Options", "nosniff")
|
||||||
|
return streamSSE(c, async (stream) => {
|
||||||
|
stream.writeSSE({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: "server.connected",
|
||||||
|
properties: {},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
response.headers.set(
|
const unsub = Bus.subscribeAll(async (event) => {
|
||||||
"Content-Security-Policy",
|
await stream.writeSSE({
|
||||||
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:",
|
data: JSON.stringify(event),
|
||||||
|
})
|
||||||
|
if (event.type === Bus.InstanceDisposed.type) {
|
||||||
|
stream.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send heartbeat every 10s to prevent stalled proxy streams.
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
stream.writeSSE({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: "server.heartbeat",
|
||||||
|
properties: {},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}, 10_000)
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
stream.onAbort(() => {
|
||||||
|
clearInterval(heartbeat)
|
||||||
|
unsub()
|
||||||
|
resolve()
|
||||||
|
log.info("event disconnected")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return response
|
// .route("/pty", PtyRoutes(ws.upgradeWebSocket))
|
||||||
})
|
|
||||||
|
return {
|
||||||
|
app: route as Hono,
|
||||||
|
ws,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openapi() {
|
export async function openapi() {
|
||||||
@@ -530,52 +588,89 @@ export namespace Server {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated do not use this dumb shit */
|
|
||||||
export let url: URL
|
export let url: URL
|
||||||
|
|
||||||
export function listen(opts: {
|
export async function listen(opts: {
|
||||||
port: number
|
port: number
|
||||||
hostname: string
|
hostname: string
|
||||||
mdns?: boolean
|
mdns?: boolean
|
||||||
mdnsDomain?: string
|
mdnsDomain?: string
|
||||||
cors?: string[]
|
cors?: string[]
|
||||||
}) {
|
}): Promise<Listener> {
|
||||||
url = new URL(`http://${opts.hostname}:${opts.port}`)
|
const log = Log.create({ service: "server" })
|
||||||
const app = createApp(opts)
|
const built = create({
|
||||||
const args = {
|
...opts,
|
||||||
hostname: opts.hostname,
|
})
|
||||||
idleTimeout: 0,
|
const start = (port: number) =>
|
||||||
fetch: app.fetch,
|
new Promise<ServerType>((resolve, reject) => {
|
||||||
websocket: websocket,
|
const server = createAdaptorServer({ fetch: built.app.fetch })
|
||||||
} as const
|
built.ws.injectWebSocket(server)
|
||||||
const tryServe = (port: number) => {
|
const fail = (err: Error) => {
|
||||||
try {
|
cleanup()
|
||||||
return Bun.serve({ ...args, port })
|
reject(err)
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
const ready = () => {
|
||||||
|
cleanup()
|
||||||
|
resolve(server)
|
||||||
}
|
}
|
||||||
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
|
const cleanup = () => {
|
||||||
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
|
server.off("error", fail)
|
||||||
|
server.off("listening", ready)
|
||||||
|
}
|
||||||
|
server.once("error", fail)
|
||||||
|
server.once("listening", ready)
|
||||||
|
server.listen(port, opts.hostname)
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
|
||||||
|
const addr = server.address()
|
||||||
|
if (!addr || typeof addr === "string") {
|
||||||
|
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL("http://localhost")
|
||||||
|
url.hostname = opts.hostname
|
||||||
|
url.port = String(addr.port)
|
||||||
|
Server.url = url
|
||||||
|
|
||||||
const shouldPublishMDNS =
|
const shouldPublishMDNS =
|
||||||
opts.mdns &&
|
opts.mdns &&
|
||||||
server.port &&
|
addr.port &&
|
||||||
opts.hostname !== "127.0.0.1" &&
|
opts.hostname !== "127.0.0.1" &&
|
||||||
opts.hostname !== "localhost" &&
|
opts.hostname !== "localhost" &&
|
||||||
opts.hostname !== "::1"
|
opts.hostname !== "::1"
|
||||||
if (shouldPublishMDNS) {
|
if (shouldPublishMDNS) {
|
||||||
MDNS.publish(server.port!, opts.mdnsDomain)
|
MDNS.publish(addr.port, opts.mdnsDomain)
|
||||||
} else if (opts.mdns) {
|
} else if (opts.mdns) {
|
||||||
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalStop = server.stop.bind(server)
|
let closing: Promise<void> | undefined
|
||||||
server.stop = async (closeActiveConnections?: boolean) => {
|
return {
|
||||||
|
hostname: opts.hostname,
|
||||||
|
port: addr.port,
|
||||||
|
url,
|
||||||
|
stop(close?: boolean) {
|
||||||
|
closing ??= new Promise((resolve, reject) => {
|
||||||
if (shouldPublishMDNS) MDNS.unpublish()
|
if (shouldPublishMDNS) MDNS.unpublish()
|
||||||
return originalStop(closeActiveConnections)
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
if (close) {
|
||||||
|
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
||||||
|
server.closeAllConnections()
|
||||||
|
}
|
||||||
|
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
||||||
|
server.closeIdleConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return closing
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { STATUS_CODES } from "http"
|
|||||||
import { Storage } from "@/storage/storage"
|
import { Storage } from "@/storage/storage"
|
||||||
import { ProviderError } from "@/provider/error"
|
import { ProviderError } from "@/provider/error"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { type SystemError } from "bun"
|
import type { SystemError } from "bun"
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
import { ModelID, ProviderID } from "@/provider/schema"
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export namespace ToolRegistry {
|
|||||||
if (matches.length) await Config.waitForDependencies()
|
if (matches.length) await Config.waitForDependencies()
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const namespace = path.basename(match, path.extname(match))
|
const namespace = path.basename(match, path.extname(match))
|
||||||
const mod = await import(pathToFileURL(match).href)
|
const mod = await import(process.platform === "win32" ? match : pathToFileURL(match).href)
|
||||||
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
|
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
|
||||||
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
|
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export namespace Process {
|
|||||||
|
|
||||||
const proc = launch(cmd[0], cmd.slice(1), {
|
const proc = launch(cmd[0], cmd.slice(1), {
|
||||||
cwd: opts.cwd,
|
cwd: opts.cwd,
|
||||||
|
shell: opts.shell,
|
||||||
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
||||||
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
||||||
shell: opts.shell,
|
|
||||||
windowsHide: process.platform === "win32",
|
windowsHide: process.platform === "win32",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user