Просмотр исходного кода

feat(cli): add instance: false opt-out to effectCmd (#25507)

Kit Langton 4 месяцев назад
Родитель
Сommit
e98c291866
2 измененных файлов с 44 добавлено и 17 удалено
  1. 11 8
      packages/opencode/src/cli/cmd/serve.ts
  2. 33 9
      packages/opencode/src/cli/effect-cmd.ts

+ 11 - 8
packages/opencode/src/cli/cmd/serve.ts

@@ -1,21 +1,24 @@
+import { Effect } from "effect"
 import { Server } from "../../server/server"
 import { Server } from "../../server/server"
-import { cmd } from "./cmd"
+import { effectCmd } from "../effect-cmd"
 import { withNetworkOptions, resolveNetworkOptions } from "../network"
 import { withNetworkOptions, resolveNetworkOptions } from "../network"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Flag } from "@opencode-ai/core/flag/flag"
 
 
-export const ServeCommand = cmd({
+export const ServeCommand = effectCmd({
   command: "serve",
   command: "serve",
   builder: (yargs) => withNetworkOptions(yargs),
   builder: (yargs) => withNetworkOptions(yargs),
   describe: "starts a headless opencode server",
   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) {
     if (!Flag.OPENCODE_SERVER_PASSWORD) {
       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 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}`)
     console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
 
 
-    await new Promise(() => {})
-    await server.stop()
-  },
+    yield* Effect.never
+  }),
 })
 })

+ 33 - 9
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 }))
 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
  * Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
  * an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
  * 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
  * `effectCmd`, swapping the underlying `cmd()` factory for effect/cli's
  * `Command.make(...)` won't touch any handler bodies.
  * `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>({
   cmd<{}, Args>({
     command: opts.command,
     command: opts.command,
     aliases: opts.aliases,
     aliases: opts.aliases,
@@ -52,6 +72,10 @@ export const effectCmd = <Args, A>(opts: {
     async handler(rawArgs) {
     async handler(rawArgs) {
       // yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
       // yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
       const args = rawArgs as unknown as Args
       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()
       const directory = opts.directory?.(args) ?? process.cwd()
       await AppRuntime.runPromise(
       await AppRuntime.runPromise(
         InstanceStore.Service.use((store) =>
         InstanceStore.Service.use((store) =>