@wy-ai-labs/env-config
v0.1.0
Published
Dependency-free .env loader: the real environment wins, legacy key inheritance, typed getters that never log values
Maintainers
Readme
@wy-ai-labs/env-config
Dependency-free
.envloader that lets the real environment win, keeps legacy variable names alive, and reads typed values without ever printing one.
Part of the wy-ai-labs parts monorepo (packages/env-config) · contract v1 · no Python twin
Install
npm install @wy-ai-labs/env-config # Node ≥22.11, ESM only (import — no require)30-second usage
import { resolveEnvFile, loadEnv, inheritLegacy, envUrl, envInt, envBool, CONTRACT_VERSION } from '@wy-ai-labs/env-config';
// 1. which file? explicit flag > MYAPP_ENV_FILE > ./.env (first that exists) — null when none applies
const envFile = resolveEnvFile({ explicit: process.argv[2], envVar: 'MYAPP_ENV_FILE' });
// 2. apply it: a variable that is already set is never overwritten; a missing file is not an error
const { loaded, applied, skipped } = loadEnv({ path: envFile ?? '.env' });
// 3. renamed product? keep listening to the old names (never overrides a set key)
inheritLegacy(process.env, { MYAPP_API_KEY: ['OLDAPP_API_KEY'], MYAPP_BASE_URL: ['OLDAPP_BASE_URL'] });
// 4. read typed values — errors name the key and the rule, never the value
const baseUrl = envUrl(process.env, 'MYAPP_BASE_URL', { default: 'http://127.0.0.1:1234/v1', protocols: ['http', 'https'] });
const port = envInt(process.env, 'PORT', { default: 3000, min: 1, max: 65535 });
const debug = envBool(process.env, 'DEBUG', { default: false });
console.log(CONTRACT_VERSION, { loaded, applied, skipped, baseUrl, port, debug });Runs as-is, no endpoint needed: without a .env it prints loaded: false and the defaults. Two more shapes you will reach for:
import { parseDotenv, loadEnv, envList, envInt, EnvConfigError } from '@wy-ai-labs/env-config';
parseDotenv('export KEY="a b" # comment\nLIST=x, y ,z\n'); // { KEY: 'a b', LIST: 'x, y ,z' }
const dry = loadEnv({ path: '.env', apply: false }); // report only — process.env untouched
dry.values; // what the file would contribute
envList({ LIST: 'x, y ,z' }, 'LIST'); // [ 'x', 'y', 'z' ]
try { envInt({ PORT: 'eighty' }, 'PORT'); } catch (e) { e instanceof EnvConfigError && e.code; } // 'invalid' — message names PORT, not 'eighty'Contract
Public API, invariants and error model are fixed in CONTRACT.md; the tests in test/ are the contract suite (one test per numbered invariant). Three invariants to know before depending on this part:
- The real environment wins —
loadEnvnever overwrites a variable that is already set (even to"") unless you passoverride: true; such keys are reported inskipped. - Missing file, no throw; values, no print — a non-existent file yields
loaded: false(onlyrequired: trueturns it intoEnvConfigError{ code: 'missing' }), and no getter, warning or error message ever contains a variable's value. - No side effects at import — importing the module reads no file and writes nothing; every function works on the
envobject it is given (process.envonly by default).
An incompatible change bumps CONTRACT_VERSION and the major version together (CONTRACT.md → Compatibility).
Mined from
Extracted from three private source repositories — AgentRAGKnowledge (module src/env/loadEnv.js and its runtime-.env test), CodeReviewWar (electron/updater.cjs#parseDotenv, server/runtime/server-runtime.ts#loadServerConfig and its env-file test) and MyWork (MailDoAI) (src/maildoai/config.py: env precedence and legacy-name inheritance semantics only) — developed 2026-06-30 – 2026-08-21, generalized and re-tested for publication. What was kept, generalized and stripped: PROVENANCE.md.
Used by
| Client | Role of this part there |
|---|---|
| llm-gateway (blueprint desktop-tray-service, planned) | per-user .env applied at boot under the real environment; typed port / URL settings |
| ark (blueprint desktop-rag-workbench, planned) | runtime .env for embedding endpoint overrides without a rebuild; ENV_FILE-style path override |
| figma-tables · pr-digest (planned) | token / endpoint configuration with legacy-name inheritance |
Dependencies & budget
- Runtime dependencies: 0 (budget 0) — enforced by
scripts/check-budget.mjs. - Allowed: other
@wy-ai-labs/*parts, pinned as caret ranges from the registry. Never a client, neverfile:/link:/ git URLs /../. - No private host, model id or key as a default: this part defines no variable names of its own — the caller chooses the env var that points at the file (
envVar), the legacy-name map and the keys it reads; loopback addresses appear in docs only.
Gates
| Command | What | When |
|---|---|---|
| npm test | contract suite (node:test), offline, seconds | every save |
| npm run gates:core | dependency budget + independence + engines.node + tests | before every commit / PR |
| npm run gates:full | everything above | nightly / before release |
CI only calls these scripts (wy-ai-labs/.github → node-gates.yml). Releases: Keep a Changelog + npm publish --provenance.
License
MIT © 2026 waneekim
한국어 요약
- @wy-ai-labs/env-config — 의존성 없는
.env로더: 실제 환경변수가 항상 우선하고, 옛 변수 이름을 계속 듣고(레거시 상속), 값을 절대 출력하지 않는 타입 getter(envString/envInt/envBool/envList/envUrl)를 제공합니다. 공개 API·불변식·오류 모델은 CONTRACT.md에 고정되어 있고, 테스트가 곧 계약 스위트입니다. - 설치
npm install @wy-ai-labs/env-config(Node ≥22, ESM). 런타임 의존성 0, 다른 part에만 핀 버전으로 의존, 사설 호스트 기본값 없음, import 시 부수효과 없음. - 비공개 저장소
AgentRAGKnowledge·CodeReviewWar·MyWork (MailDoAI)에서 추출·일반화했습니다(PROVENANCE.md). 커밋 전npm run gates:core.
