@effected/env
v0.5.0
Published
Effect-native environment detection: agent, CI, terminal colour level, hyperlinks, columns and audience — read once through Config, swapped in tests with layerTest
Maintainers
Readme
@effected/env
Effect-native environment detection for any front end of a tool: who is running it (an AI agent, a CI job, or neither), what the terminal can do (colour level, OSC 8 hyperlinks and width, per stream), and who the output is for (a human, an agent or CI). Every answer is read once through Config, never from process, and every service has a layerTest, so a test sets the environment it wants without touching a global.
Pre-
1.0.0. This package is part of the@effected/*kit, built on stable Effect v4 (effect^4.0.0) and still in0.xdevelopment. Stable Effect makes a kit1.0.0possible, not automatic. To keep youreffectand@effect/*versions on the line the kit is built and tested against, install@effected/pnpm-plugin-effect.Stability: unstable. This package's API surface is not yet considered complete and may change across
0.xreleases. Pin an exact version. Full policy: release strategy.
Why @effected/env
Every tool that prints ends up asking these questions, usually by reading process.env and process.stdout.isTTY wherever the answer is needed. The answers then drift (the help text and the report disagree about colour), they cannot be tested without mutating globals, and the rules are subtle: FORCE_COLOR beats NO_COLOR, an empty NO_COLOR means unset, TERM=dumb is a terminal that cannot move the cursor, a Windows console sets no TERM at all yet draws truecolor, and an agent running inside CI should get agent output.
This package makes each decision once, as a service, from rules taken from established sources: colour depth from Node's own getColorDepth (with OS=Windows_NT standing in for its platform check, which gives a Windows terminal truecolor as Node does from Windows 10 build 14931), hyperlink support from std-osc8's terminal table, and agent detection from std-env's table.
Install
npm install @effected/env effectpnpm add @effected/env effectRequires Node.js >=24.11.0. effect v4 is the only peer. There are no runtime dependencies, no platform import and no @effected/* edge, so it runs unchanged on Node, Bun and Deno.
All @effected/* packages are ESM-only: the exports maps publish only import conditions, so require() fails with Node's ERR_PACKAGE_PATH_NOT_EXPORTED. Import from an ES module.
Quick start
Decide the audience once, as a service, and let anything that prints ask it:
import { Audience, CurrentRuntimeEnv } from "@effected/env";
import { Effect, Layer } from "effect";
// A valid MYTOOL_AUDIENCE beats detection; then an agent, then CI, then a human.
const AudienceLive = Audience.layer({ envVar: "MYTOOL_AUDIENCE" }).pipe(Layer.provide(CurrentRuntimeEnv.layer));
const program = Effect.gen(function* () {
const audience = yield* Audience;
if (audience.kind === "agent") {
// plain text, no escapes
}
});
Effect.runPromise(program.pipe(Effect.provide(AudienceLive)));What the terminal can do comes from TerminalEnv, which reads core's Stdio and Terminal from your platform layer:
import { TerminalEnv } from "@effected/env";
import { NodeServices } from "@effect/platform-node";
import { Effect } from "effect";
const describe = Effect.gen(function* () {
const terminal = yield* TerminalEnv;
return `${terminal.stdout.color} colour, ${terminal.width()} columns`;
});
Effect.runPromise(describe.pipe(Effect.provide(TerminalEnv.layer()), Effect.provide(NodeServices.layer)));In a test, fix the answers instead:
import { Audience, TerminalEnv } from "@effected/env";
import { Layer, Option } from "effect";
const Environment = Layer.mergeAll(
Audience.layerTest("agent"),
TerminalEnv.layerTest({ stdout: { isTerminal: true, color: "256", columns: Option.some(120) } }),
);What it exports
RuntimeEnv: a schema-class snapshot of the agent, the CI and the terminal program, persistable as plain JSON.RuntimeEnv.fromRecord(env)builds one as a pure function.CurrentRuntimeEnv: that snapshot as a service.layerreads the ambientConfigProvideronce;layerFrom(source)reads a record or provider of your own, fresh on every use, for a long-lived host;layerTest(overrides)fixes it.TerminalEnv: per-streamisTerminal, colour level (none,basic,256,truecolor),hyperlinksandcolumns, pluswidth(fallback).layer()needsStdioandTerminal;layerStdio()needs onlyStdio;colorLevel("stdout")decides colour alone.Audience:human,agentorci, and thesourcethat decided it.layer({ envVar })decides fromCurrentRuntimeEnvand an override variable you name.EnvOverride: reads a variable that picks a mode within an audience, accepting only the literals you list per audience.readResultreports without logging;readlogs one warning for a rejected value.
Documentation
The guides and the full API reference are at effected.spencerbeg.gs/env. @effected/cli builds its whole presentation layer on these services.
