Skip to content

Commit fe0c4f8

Browse files
authored
refactor(server): canonicalize service API (anomalyco#31049)
1 parent 53ff1b5 commit fe0c4f8

388 files changed

Lines changed: 7075 additions & 4064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 43 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/bunfig.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
preload = ["@opentui/solid/preload"]

packages/cli/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
"@opencode-ai/core": "workspace:*",
2121
"@opencode-ai/sdk": "workspace:*",
2222
"@opencode-ai/server": "workspace:*",
23+
"@opencode-ai/tui": "workspace:*",
24+
"@opentui/core": "catalog:",
25+
"@opentui/solid": "catalog:",
2326
"@parcel/watcher": "2.5.1",
24-
"effect": "catalog:"
27+
"effect": "catalog:",
28+
"solid-js": "catalog:"
2529
},
2630
"devDependencies": {
2731
"@opencode-ai/script": "workspace:*",

packages/cli/script/build.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
#!/usr/bin/env bun
22

3+
import { $ } from "bun"
4+
import fs from "fs"
35
import { rm } from "fs/promises"
46
import path from "path"
57
import { Script } from "@opencode-ai/script"
8+
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
9+
import pkg from "../package.json"
610
import { modelsData } from "./generate"
711

812
const dir = path.resolve(import.meta.dirname, "..")
@@ -13,7 +17,9 @@ await rm("dist", { recursive: true, force: true })
1317

1418
const singleFlag = process.argv.includes("--single")
1519
const baselineFlag = process.argv.includes("--baseline")
20+
const skipInstall = process.argv.includes("--skip-install")
1621
const sourcemapsFlag = process.argv.includes("--sourcemaps")
22+
const plugin = createSolidTransformPlugin()
1723

1824
const allTargets: {
1925
os: string
@@ -43,6 +49,12 @@ const targets = singleFlag
4349
})
4450
: allTargets
4551

52+
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
53+
54+
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
55+
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
56+
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
57+
4658
for (const item of targets) {
4759
const target = [
4860
binary,
@@ -56,8 +68,9 @@ for (const item of targets) {
5668
const name = target.replace(binary, "cli")
5769
console.log(`building ${name}`)
5870
const result = await Bun.build({
59-
entrypoints: ["./src/index.ts"],
71+
entrypoints: ["./src/index.ts", parserWorker],
6072
tsconfig: "./tsconfig.json",
73+
plugins: [plugin],
6174
external: ["node-gyp"],
6275
format: "esm",
6376
minify: true,
@@ -79,6 +92,11 @@ for (const item of targets) {
7992
OPENCODE_MODELS_DEV: modelsData,
8093
OPENCODE_CHANNEL: `'${Script.channel}'`,
8194
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
95+
OTUI_TREE_SITTER_WORKER_PATH:
96+
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
97+
path.relative(dir, parserWorker).replaceAll("\\", "/") +
98+
'"',
99+
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
82100
},
83101
})
84102

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Commands } from "../commands"
2+
import { Runtime } from "../../framework/runtime"
3+
import { Effect } from "effect"
4+
import { Daemon } from "../../services/daemon"
5+
6+
export default Runtime.handler(Commands, () =>
7+
Effect.gen(function* () {
8+
const daemon = yield* Daemon.Service
9+
const transport = yield* daemon.transport()
10+
const { runTui } = yield* Effect.promise(() => import("../../tui"))
11+
yield* Effect.promise(() => runTui(transport))
12+
}),
13+
)

packages/cli/src/commands/handlers/serve.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NodeHttpServer } from "@effect/platform-node"
2+
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
23
import { Context, Layer, Option } from "effect"
34
import * as Effect from "effect/Effect"
45
import { HttpRouter, HttpServer } from "effect/unstable/http"
@@ -34,6 +35,7 @@ function bind(hostname: string, port: number, password: string) {
3435
return Layer.build(
3536
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
3637
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
38+
Layer.provide(PermissionSaved.defaultLayer),
3739
),
3840
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
3941
}

packages/cli/src/framework/runtime.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,19 @@ export function run(commands: Spec.Any, handlers: ReadonlyArray<LazyHandler>, op
6060
}
6161

6262
function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
63-
const spec: Command.Command.Any = Object.keys(node.commands).length
64-
? (node.spec as Command.Command<string, unknown>).pipe(
65-
Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
63+
const handler = handlers.find((handler) => handler.spec === node.spec)
64+
const spec = handler
65+
? node.spec.pipe(
66+
Command.withHandler((input) =>
67+
Effect.gen(function* () {
68+
yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
69+
}),
70+
),
6671
)
6772
: node.spec
68-
const handler = handlers.find((handler) => handler.spec === node.spec)
69-
if (!handler) return spec as ProvidedCommand
73+
if (!Object.keys(node.commands).length) return spec as ProvidedCommand
7074
return spec.pipe(
71-
Command.withHandler((input) =>
72-
Effect.gen(function* () {
73-
yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
74-
}),
75-
),
75+
Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
7676
) as ProvidedCommand
7777
}
7878

packages/cli/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Runtime } from "./framework/runtime"
88
import { Daemon } from "./services/daemon"
99

1010
const Handlers = Runtime.handlers(Commands, {
11+
$: () => import("./commands/handlers/default"),
1112
debug: {
1213
agents: () => import("./commands/handlers/debug/agents"),
1314
},

packages/cli/src/services/daemon.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { ServerAuth } from "@opencode-ai/server/auth"
55
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
66
import { HttpServer } from "effect/unstable/http"
77
import { randomBytes, randomUUID } from "crypto"
8+
import { spawn } from "node:child_process"
89
import path from "path"
910

1011
export interface Interface {
1112
readonly client: () => Effect.Effect<ReturnType<typeof createOpencodeClient>, unknown>
13+
readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown>
1214
readonly start: () => Effect.Effect<string, Error>
1315
readonly status: () => Effect.Effect<string | undefined>
1416
readonly stop: () => Effect.Effect<void, unknown>
@@ -108,16 +110,20 @@ export const layer = Layer.effect(
108110
const start = Effect.fn("cli.daemon.start")(function* () {
109111
const existing = yield* healthy().pipe(Effect.option)
110112
const found = Option.getOrUndefined(existing)
111-
if (found?.version === InstallationVersion) return found.url
113+
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
114+
if (found?.version === InstallationVersion && compiled) return found.url
112115
if (found) yield* stopProcess(found).pipe(Effect.ignore)
113116

114-
yield* Effect.sync(() => {
115-
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
116-
Bun.spawn([process.execPath, ...(compiled ? [] : [Bun.main]), "serve", "--register"], {
117-
stdin: "ignore",
118-
stdout: "ignore",
119-
stderr: "ignore",
120-
}).unref()
117+
const entrypoint = compiled ? undefined : process.argv[1]
118+
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
119+
yield* Effect.try({
120+
try: () => {
121+
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {
122+
detached: true,
123+
stdio: "ignore",
124+
}).unref()
125+
},
126+
catch: (cause) => new Error("Failed to start server", { cause }),
121127
})
122128

123129
return yield* compatible().pipe(
@@ -127,8 +133,13 @@ export const layer = Layer.effect(
127133
)
128134
})
129135

136+
const transport = Effect.fn("cli.daemon.transport")(function* () {
137+
return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
138+
})
139+
130140
const client = Effect.fn("cli.daemon.client")(function* () {
131-
return yield* createClient(yield* start())
141+
const connection = yield* transport()
142+
return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers })
132143
})
133144

134145
const status = Effect.fn("cli.daemon.status")(function* () {
@@ -173,7 +184,7 @@ export const layer = Layer.effect(
173184
)
174185
})
175186

176-
return Service.of({ client, start, status, stop, password, register })
187+
return Service.of({ client, transport, start, status, stop, password, register })
177188
}),
178189
)
179190

0 commit comments

Comments
 (0)