summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-05-02 21:44:06 -0400
committerGitHub <[email protected]>2026-05-03 01:44:06 +0000
commite98c291866f4b3e48caa3dbeb39386dd884a45bd (patch)
treed243df5843d631af5a46eb6b164f9edc39496c3f
parente709dc34fb795dfa35d49d67673baa7b0f56dac8 (diff)
downloadopencode-e98c291866f4b3e48caa3dbeb39386dd884a45bd.tar.gz
opencode-e98c291866f4b3e48caa3dbeb39386dd884a45bd.zip
feat(cli): add instance: false opt-out to effectCmd (#25507)
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts19
-rw-r--r--packages/opencode/src/cli/effect-cmd.ts42
2 files changed, 44 insertions, 17 deletions
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 5f3211aa1..a8a7234d9 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/packages/opencode/src/cli/cmd/serve.ts
@@ -1,21 +1,24 @@
+import { Effect } from "effect"
import { Server } from "../../server/server"
-import { cmd } from "./cmd"
+import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
-export const ServeCommand = cmd({
+export const ServeCommand = effectCmd({
command: "serve",
builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
- handler: async (args) => {
+ // Server loads instances per-request via x-opencode-directory header — no
+ // need for an ambient project InstanceContext at startup.
+ instance: false,
+ handler: Effect.fn("Cli.serve")(function* (args) {
if (!Flag.OPENCODE_SERVER_PASSWORD) {
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
- const opts = await resolveNetworkOptions(args)
- const server = await Server.listen(opts)
+ const opts = yield* Effect.promise(() => resolveNetworkOptions(args))
+ const server = yield* Effect.promise(() => Server.listen(opts))
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
- await new Promise(() => {})
- await server.stop()
- },
+ yield* Effect.never
+ }),
})
diff --git a/packages/opencode/src/cli/effect-cmd.ts b/packages/opencode/src/cli/effect-cmd.ts
index 6785e0b61..94ad0232c 100644
--- a/packages/opencode/src/cli/effect-cmd.ts
+++ b/packages/opencode/src/cli/effect-cmd.ts
@@ -18,6 +18,34 @@ export class CliError extends Schema.TaggedErrorClass<CliError>()("CliError", {
export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError({ message, exitCode }))
+interface EffectCmdOpts<Args, A> {
+ command: string | readonly string[]
+ aliases?: string | readonly string[]
+ describe: string | false
+ builder?: (yargs: Argv) => Argv<Args>
+ /**
+ * Whether the command needs a project InstanceContext. Defaults to true.
+ *
+ * `true` (default): wraps the handler in `InstanceStore.Service.provide({directory})`
+ * so `InstanceRef` resolves to a loaded `InstanceContext`. Auto-disposes via
+ * `Effect.ensuring(store.dispose(ctx))` on every Exit (matches the legacy
+ * `bootstrap()` finally-disposal). Runs InstanceBootstrap (config + plugin
+ * init + LSP/File/etc forks) eagerly.
+ *
+ * `false`: skip the instance entirely. Saves the InstanceBootstrap work and
+ * suppresses the `server.instance.disposed` IPC event. The handler runs
+ * directly under AppRuntime — it can yield any `AppServices` but must not
+ * yield `InstanceRef` (it'd be undefined, causing a defect).
+ *
+ * Use `false` for commands that don't read project state (e.g. `models`,
+ * `serve`, `web`, `account`, `db`, `upgrade`).
+ */
+ instance?: boolean
+ /** Defaults to process.cwd(). Override for commands that take a directory positional. */
+ directory?: (args: Args) => string
+ handler: (args: Args) => Effect.Effect<A, CliError, AppServices | InstanceStore.Service>
+}
+
/**
* Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
* an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
@@ -35,15 +63,7 @@ export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError(
* `effectCmd`, swapping the underlying `cmd()` factory for effect/cli's
* `Command.make(...)` won't touch any handler bodies.
*/
-export const effectCmd = <Args, A>(opts: {
- command: string | readonly string[]
- aliases?: string | readonly string[]
- describe: string | false
- builder?: (yargs: Argv) => Argv<Args>
- /** Defaults to process.cwd(). Override for commands that take a directory positional. */
- directory?: (args: Args) => string
- handler: (args: Args) => Effect.Effect<A, CliError, AppServices | InstanceStore.Service>
-}) =>
+export const effectCmd = <Args, A>(opts: EffectCmdOpts<Args, A>) =>
cmd<{}, Args>({
command: opts.command,
aliases: opts.aliases,
@@ -52,6 +72,10 @@ export const effectCmd = <Args, A>(opts: {
async handler(rawArgs) {
// yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
const args = rawArgs as unknown as Args
+ if (opts.instance === false) {
+ await AppRuntime.runPromise(opts.handler(args))
+ return
+ }
const directory = opts.directory?.(args) ?? process.cwd()
await AppRuntime.runPromise(
InstanceStore.Service.use((store) =>