Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton
dc7cff5fd3 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).
2026-05-02 16:22:59 -04:00

View File

@@ -2,6 +2,7 @@ import type { Argv } from "yargs"
import { Effect, Schema } from "effect"
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
import { InstanceStore } from "@/project/instance-store"
import { InstanceRef } from "@/effect/instance-ref"
import { cmd } from "./cmd/cmd"
/**
@@ -21,6 +22,11 @@ export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError(
* Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
* an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
*
* The handler is wrapped in `Effect.ensuring(store.dispose(ctx))` so the loaded
* InstanceContext is disposed (runDisposers + IPC `server.instance.disposed`)
* on every Exit — success, typed failure, defect, or interruption. Matches the
* legacy `bootstrap()` finally-disposal semantics without per-handler boilerplate.
*
* Errors propagate to the existing top-level handler in `src/index.ts`; use
* `fail("...")` for user-visible domain failures (clean exit, formatted message).
*
@@ -45,6 +51,17 @@ export const effectCmd = <Args, A>(opts: {
// yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
const args = rawArgs as unknown as Args
const directory = opts.directory?.(args) ?? process.cwd()
await AppRuntime.runPromise(InstanceStore.Service.use((s) => s.provide({ directory }, opts.handler(args))))
await AppRuntime.runPromise(
InstanceStore.Service.use((store) =>
store.provide(
{ directory },
Effect.gen(function* () {
const ctx = yield* InstanceRef
const body = opts.handler(args)
return ctx ? yield* body.pipe(Effect.ensuring(store.dispose(ctx))) : yield* body
}),
),
),
)
},
})