Skip to content

Commit 00bee6c

Browse files
committed
feat(cli): auto-dispose InstanceContext after effectCmd handlers
Wrap the user handler in Effect.ensuring(store.dispose(ctx)) inside the effectCmd factory so per-command disposal is automatic — runs disposers + emits server.instance.disposed on every Exit (success / typed failure / defect / interruption). Matches the legacy bootstrap() finally without 14× hand-rolled boilerplate at the call sites. Idempotent: existing commands that still call store.dispose(ctx) explicitly inside their handler bodies will see the second auto-dispose call become a no-op (cache entry already removed; disposeContext doesn't re-fire).
1 parent 9bef88e commit 00bee6c

1 file changed

Lines changed: 18 additions & 1 deletion

File tree

packages/opencode/src/cli/effect-cmd.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Argv } from "yargs"
22
import { Effect, Schema } from "effect"
33
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
44
import { InstanceStore } from "@/project/instance-store"
5+
import { InstanceRef } from "@/effect/instance-ref"
56
import { cmd } from "./cmd/cmd"
67

78
/**
@@ -21,6 +22,11 @@ export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError(
2122
* Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
2223
* an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
2324
*
25+
* The handler is wrapped in `Effect.ensuring(store.dispose(ctx))` so the loaded
26+
* InstanceContext is disposed (runDisposers + IPC `server.instance.disposed`)
27+
* on every Exit — success, typed failure, defect, or interruption. Matches the
28+
* legacy `bootstrap()` finally-disposal semantics without per-handler boilerplate.
29+
*
2430
* Errors propagate to the existing top-level handler in `src/index.ts`; use
2531
* `fail("...")` for user-visible domain failures (clean exit, formatted message).
2632
*
@@ -47,6 +53,17 @@ export const effectCmd = <Args, A>(opts: {
4753
// yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
4854
const args = rawArgs as unknown as Args
4955
const directory = opts.directory?.(args) ?? process.cwd()
50-
await AppRuntime.runPromise(InstanceStore.Service.use((s) => s.provide({ directory }, opts.handler(args))))
56+
await AppRuntime.runPromise(
57+
InstanceStore.Service.use((store) =>
58+
store.provide(
59+
{ directory },
60+
Effect.gen(function* () {
61+
const ctx = yield* InstanceRef
62+
const body = opts.handler(args)
63+
return ctx ? yield* body.pipe(Effect.ensuring(store.dispose(ctx))) : yield* body
64+
}),
65+
),
66+
),
67+
)
5168
},
5269
})

0 commit comments

Comments
 (0)