1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import { detectCapabilities } from "@alchemy.run/sigil/capabilities";
4import { isNonInteractive } from "../../Util/interactive.ts";
5import { CliKit } from "./CliKit.ts";
6import type { CliKitCapabilities, CliKitOptions } from "../components/types.ts";
7
8export const resolveCapabilities = (
9 options: CliKitOptions,
10): CliKitCapabilities => {
11 const stdout = options.stdout ?? process.stdout;
12 const stdin = options.stdin ?? process.stdin;
13 const detected = detectCapabilities({ stdout });
14 const input =
15 options.input ??
16 (stdin.isTTY === true && stdout.isTTY === true && !isNonInteractive());
17 return {
18 input,
19 columns: detected.size.columns,
20 rows: detected.size.rows,
21 colors: options.colors ?? detected.supports.color,
22 unicode: options.unicode ?? detected.supports.unicode,
23 alternateScreen: detected.supports.alternateScreen,
24 };
25};
26
27// One shared import promise per process. Concurrent dynamic imports of the
28// same module (e.g. two test files building this layer at once) can race in
29// bun and hand one caller a partially-evaluated namespace, which throws a
30// TDZ ReferenceError on `makeRuntime`; funneling every build through a
31// single import() sidesteps the race.
32let sigilRuntime:
33 | Promise<typeof import("../components/view/Runtime.tsx")>
34 | undefined;
35const loadSigilRuntime = () =>
36 (sigilRuntime ??= import("../components/view/Runtime.tsx"));
37
38/** Provides one terminal runtime for the enclosing scope. */
39export const layer = (options: CliKitOptions = {}) =>
40 Layer.effect(
41 CliKit,
42 Effect.acquireRelease(
43 Effect.promise(async () => {
44 const capabilities = resolveCapabilities(options);
45 const { makeRuntime } = await loadSigilRuntime();
46 return makeRuntime(options, capabilities);
47 }),
48 ({ dispose }) => Effect.promise(dispose),
49 ).pipe(Effect.map(({ service }) => service)),
50 );