std-env — runtime-agnostic JS utility library for detecting environments, runtimes, CI providers, and AI coding agents. Published as std-env on npm under unjs/std-env.
AGENTS.md— Keep updated with technical details, architecture, and conventions for AI agents working on this projectREADME.md— Keep updated with user-facing documentation (usage, installation, exported APIs, examples)
When adding new features or changing behavior, update both files accordingly.
After finishing any code updates, always run pnpm lint:fix then pnpm test to auto-fix lint/formatting issues and verify everything passes.
- ESM-only (
"type": "module", single.mjsoutput) - Linted with
oxlint+oxfmt, typechecked withtsgo - Tested with
vitest - Built with
obuild
| Script | Description |
|---|---|
pnpm run build |
Build with obuild (dist/index.mjs, dist/index.d.mts) |
pnpm run dev |
Start vitest in watch mode |
pnpm run test |
Lint + typecheck + vitest with coverage |
pnpm run lint |
Run oxlint and oxfmt |
pnpm run lint:fix |
Auto-fix lint/format issues |
pnpm run typecheck |
Run tsgo --noEmit |
All source lives in src/, single entry point at src/index.ts which re-exports everything.
| File | Purpose |
|---|---|
src/index.ts |
Barrel re-export of all modules |
src/agents.ts |
AI coding agent detection (detectAgent, agentInfo, agent, isAgent) |
src/providers.ts |
CI/CD provider detection (detectProvider, providerInfo, provider) |
src/runtimes.ts |
JS runtime detection (runtime, runtimeInfo, isNode, isBun, isDeno, etc.) |
src/flags.ts |
Environment flags (isCI, isDebug, isTest, isProduction, isDevelopment, isMinimal, platform, etc.) |
src/env.ts |
Universal env proxy + nodeENV constant |
Micro-benchmarks live in test/bench/ and use mitata. Run with:
node test/bench/agents.ts
- No global
processusage. Never useglobalThis.processor bareprocessdirectly. Always importenvfromsrc/env.tsand use it to access environment variables. This ensures runtime-agnostic behavior across all environments.
All detection modules follow the same pattern:
- Define a
Nametype union of known values (extensible viastring & {}) - Define an internal tuple array mapping names to env vars (with optional metadata)
- Implement a
detect*()function that iterates tuples checking env - Export a lazy-evaluated singleton (
*Info) and convenience accessors
- Priority:
AI_AGENTenv var (generic override, normalizing known aliases such asgithub_copilot_vscode_agentandgithub_copilot_cloud_agenttocopilot) → ordered tuple scan - Internal types:
EnvCheck = string | ((env: Record<string, string | undefined>) => boolean)InternalAgent = [agentName: AgentName, envChecks: EnvCheck[]]
- Each agent maps to an array of
EnvCheckitems; the first matching check wins - When
EnvCheckis a string, the env var must be truthy; when it's a function, it receives the full env object and returns a boolean envMatcher(envKey, regex, opts?)helper creates regex-based env var matchers (e.g.,envMatcher("TERM_PROGRAM", /kiro/))opts.noTTY: truemakes the matcher only match in a non-interactive context (whenprocess.stdout.isTTYis falsy). Use for detections keyed off generic terminal env vars that an IDE sets in its integrated terminal too — e.g.kiro(TERM_PROGRAM=kiro), where a TTY means a human is at the IDE terminal, not the agent CLI (see #185)
- GitHub Copilot (
copilot): detected viaCOPILOT_AGENT(VS Code Copilot agent terminals) orCOPILOT_CLI(Copilot CLI / Copilot app). ExplicitAI_AGENT=github_copilot_vscode_agentandAI_AGENT=github_copilot_cloud_agentare normalized tocopilot. - IDEs (cursor, devin, kiro) are checked last so agents running inside them are detected first
- Exports:
detectAgent(),agentInfo(singleton),agent(name shorthand),isAgent(boolean)
- 50+ CI/CD providers detected via env vars
- Some providers have
{ ci: false }metadata (e.g., Vercel, CodeSandbox — deployment platforms, not CI) - Some providers have
{ ci: true }explicitly (e.g., AWS Amplify, Cloudflare Pages/Workers) - Special case: StackBlitz/WebContainer detected via
SHELL === "/bin/jsh"+process.versions.webcontainer
The goal: a consumer that imports only e.g. runtime or provider should not drag the agent/provider detection tables into their bundle. Two things make this work, and both must be kept in sync:
- Annotations must survive minification. The eager singletons (
agentInfo,providerInfo) — and theenvMatcher(...)calls inside theagentstable — carry/* #__PURE__ */so bundlers can drop them when unused. oxc and esbuild both strip annotation comments when minifying, so the build usesminify: "dce-only"plus anendhook that re-minifies with terser usingformat.preserve_annotations(mangles identifiers, keeps the annotations). Do not switch tominify: trueor remove the hook. - Derived exports must not pin the singletons. A
#__PURE__annotation only applies to a call/new, never a property access — so a bareexport const provider = providerInfo.nameis not droppable and keepsproviderInfo(and its table) alive in every consumer bundle. Any export whose value is derived from a singleton (directly, likeprovider/agent/isCI, or transitively, likeisMinimal/isColorSupportedwhich referenceisCI) is therefore wrapped in a#__PURE__IIFE:export const provider = /* #__PURE__ */ (() => providerInfo.name)();. When adding a new export that reads off a singleton or another such derived flag, wrap it the same way, or it will silently re-pin the table for everyone.
test/bundle.test.ts guards both invariants: it builds dist, then tree-shakes single named imports with rollup and asserts the agents/providers tables (and the #__PURE__ annotations) are present/absent as expected. Run it after any change to build.config.ts or to how a singleton-derived export is written.
- Priority order: netlify → edge-light → workerd → fastly → deno → bun → node
- Uses global objects (e.g.,
globalThis.Bun,globalThis.Deno,navigator.userAgent) isNodeis true for Node-compatible runtimes (Bun, Deno with compat); useruntime === "node"for strict checkisNub(!!process.versions.nub) detects Nub, a Node.js augmentation layer — additive, soisNodestays true andruntimestays"node"; it is deliberately NOT inruntimeChecksorRuntimeName
toBoolean()helper is defined locally (not extracted to a util)- Uses
envimported fromsrc/env.ts(no directprocessaccess) versionsis exported (used byruntimes.ts) but not re-exported fromindex.tsisCIcombines theCIenv var withproviderInfo.ci !== false
env: proxy toglobalThis.process?.envor empty objectnodeENV: currentNODE_ENVvalue (empty string if unset)
Every agent that modifies this file MUST run the self-test and update the verification comment.
pnpm run build
node -e "import('./dist/index.mjs').then(m => console.log(m.agentInfo))"If output is like { name: "..." }, it works. If output is {}, agent is NOT DETECTED.
Debug current env variables:
node -e "console.log(process.env)"Do not invent environment variables or mark a detector as verified unless those vars were actually present when the self-test ran. Unit tests may stub env vars to cover detection logic; stubbed tests do not replace the live self-test verification comment.
After running the self-test:
-
If agent was detected: add/update a comment above the agent's entry in the
agentsrecord insrc/agents.ts:// ✅ Verified by {agentName} (can be detected using ENV1, ENV2, ...)listing the env vars that were found in the environment. When updating an existing comment, append newly discovered env vars to the list — do not remove previously listed ones. -
If agent was not detected: search
process.envoutput for environment variables that match the agent name or contain a recognizable value, then add the appropriate detection entry.
When adding or removing an agent from the agents array in src/agents.ts, you must update all of the following locations to keep them in sync:
src/agents.ts—AgentNametype union and JSDoc comments onAgentNameanddetectAgent()README.md— Supported agents list in the "Agent Detection" section (inside<!-- automd:agents -->markers)AGENTS.md— If the detection pattern or conventions change, update this file accordingly