This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CLAUDE.md and AGENTS.md are kept byte-identical. CLAUDE.md is what Claude Code reads; AGENTS.md is what vendor-neutral agent tools read — Codex, OpenCode, Cursor, Copilot, and whatever follows them. Two real files, deliberately not a symlink: not every tool resolves one.
After editing either file, copy it over the other — don't repeat the edit by hand:
cp CLAUDE.md AGENTS.md # or the reverse, whichever you just editedRetyping a change is exactly how the two drift; one reflowed line or reworded clause is enough. diff CLAUDE.md AGENTS.md must print nothing. If it ever does, treat it as a defect and fix it by letting one file win wholesale — never by merging them.
envprism is a TUI-based env file manager. It discovers .env* files in a directory and shows them as a matrix (rows = variable keys, columns = files) so n-way differences are visible at a glance. Cells are editable in place; writes preserve comments, blank lines, and key order. There is also a non-interactive envprism diff subcommand for CI use.
Distributed as a Bun CLI (bunx envprism, bun add -g envprism). Node is not supported at runtime — opentui's native core links via bun:ffi and Node has no built-in equivalent.
Mirrors ../forgemap (the kirchDev CLI house style) but pivoted to Bun for opentui compatibility:
- Bun 1.3+ at runtime (
engines.bun, version pinned in.bun-version). Node is unsupported at runtime because of opentui's FFI dependency. - pnpm 11 for dev dependency management (
packageManager+pnpm-workspace.yaml). ESM only ("type": "module"). - TypeScript strict (
noUncheckedIndexedAccess,verbatimModuleSyntax,allowImportingTsExtensionswithnoEmit). - Vite (lib mode) bundles to
dist/bin/envprism.mjs(#!/usr/bin/env bunshebang via banner) anddist/index.mjs. Runtime deps are externalised invite.config.ts— updateruntimeDepsthere when adding a new runtime dep, or the bundle will inline it. - Vitest for unit tests (config inlined in
vite.config.tsviavitest/config'sdefineConfig). Vitest still runs under Node for the core tests; TUI tests (when added) will need Bun. - opentui for the TUI. Loaded behind a dynamic import in
src/commands/tui.tsso thediffsubcommand never reaches it — useful while opentui only runs on Bun. Tracks the upstream "debunification" effort: when opentui ships a Node FFI backend, runtime can move back to Node with mostly toolchain changes (.bun-versionremoved, shebang flipped,enginesupdated). App code against opentui's API stays unchanged. Discipline: no directBun.*globals orbun:ffiimports in app code; stick to opentui's API + Node-compatible libs so the migration back is mechanical. - citty for CLI parsing, consola for non-TUI output, pathe for paths.
.envparser/serializer is custom —dotenvdoesn't round-trip. Round-trip (parse → serialize === original bytes) is a hard requirement so write-back preserves comments, blank lines, and key order.
src/
bin/envprism.ts citty runMain entry, gets the shebang banner
cli.ts root command + subcommand registration
commands/
diff.ts text / JSON / --check diff output
tui.ts default command; dynamic-imports tui/app.ts
config/ `config` group: init / path / show / edit
config/
schema.ts config types + DEFAULT_CONFIG (canonical defaults)
define.ts defineEnvprismConfig + types — `envprism/config` entry (RGBA-free)
load.ts loadEnvprismConfig: walk-up from cwd, --config / ENVPRISM_CONFIG
resolve.ts mergeConfig (defu + replace/extend lists), resolveHeuristics
core/
parse.ts .env → EnvEntry[] (round-trippable)
serialize.ts EnvEntry[] → string + rebuildKvLine()
discover.ts glob .env* in cwd or --paths
base.ts base-file resolution (--base flag, .env.example, fallback)
matrix.ts keys × files matrix with cell states + sectionOf()
diff.ts drift report for `envprism diff`
mask.ts secret-key heuristic + value masking
sections.ts banner-comment → section name detection
types.ts EnvEntry / EnvFile / KvEntry types
tui/
app.ts opentui matrix view, edit prompts, undo, help overlay
index.ts public library reexports
tests/ vitest suite — 60 tests covering core/ end-to-end
examples/ .env* fixture set used to smoke the TUI / diff
The TUI app lives in a single file (src/tui/app.ts). State is one mutable State object with mode (browse / filter / prompt), focus indices, filter string, dirty set, undo stack, etc. A refresh() function rebuilds the whole sidebar + matrix on every state change — opentui handles diffing under the hood, so we don't memoise.
| Command | What it does |
|---|---|
pnpm install |
Installs deps and activates Husky hooks (prepare). |
pnpm build |
vite build → emits dist/bin/envprism.mjs and bundles. |
pnpm dev |
vite build --watch. |
pnpm lint |
oxlint . --deny-warnings. |
pnpm format |
oxfmt --check . across JS/TS/JSON/YAML/MD. |
pnpm typecheck |
tsc --noEmit (strict). |
pnpm test |
vitest run --passWithNoTests. |
pnpm test:watch |
vitest watch mode. |
pnpm check |
lint && format && typecheck — the local gate. |
pnpm check:fix |
Auto-fix lint + format. |
pnpm taze[:w] |
Interactive dependency upgrades. |
Run a single test file: pnpm vitest run tests/parse.test.ts. Single test name: pnpm vitest run -t 'key substring'.
After pnpm build, smoke-test the produced binary: bun dist/bin/envprism.mjs tui examples/ (the included examples/ fixture set has four files with realistic drift across ~31 keys grouped into five comment-banner sections).
| Key | Action |
|---|---|
↑ ↓ ← → |
Move focused cell |
| Mouse wheel | Scroll the matrix (X + Y) |
e / Enter |
Edit focused cell value |
a |
Add a new key to the focused file (key, then value) |
d |
Delete the key from the focused file |
n |
Create a new env file next to the base |
Ctrl-Z |
Undo last edit/add/delete (50-entry stack) |
Ctrl-S |
Write every dirty file to disk |
/ |
Filter keys (Esc clears, Enter keeps) |
v |
Toggle: all keys ↔ only drifting keys |
g |
Toggle: group by comment banner ↔ key prefix |
? |
Toggle keybinding overlay |
q |
Quit (press twice if there are unsaved changes) |
Ctrl-C |
Force quit |
- Lint/format are oxc (oxlint + oxfmt), not ESLint/Prettier. Configs:
.oxlintrc.json,.oxfmtrc.json. Both ignoredist/andnode_modules/;oxfmtadditionally skipsREADME.md,CHANGELOG.md,pnpm-lock.yaml. - Conventional Commits enforced via
commitlintoncommit-msg;lint-stagedrunsoxlint --fix+oxfmtonpre-commit. Don't bypass with--no-verifyunless explicitly asked. Subject must be lowercase. - Release-please is wired up (workflow +
release-please-config.json+.release-please-manifest.jsonat0.0.0). Conventional commits onmaindrive version bumps.release-type: simple, tags includevprefix. - CI (
.github/workflows/ci.yml) runslint/format+typecheck+test+buildon PRs (skips drafts).dev-pr.ymlopens / updates a draft PR from thedevbranch intomainon every push. CodeQL runs on push/PR + weekly. pnpm-workspace.yamlholdsallowBuilds: esbuild: true— pnpm 11 blocks build scripts by default; vite needs esbuild's postinstall.- Dependabot: npm weekly, GitHub Actions monthly.
taze.config.jsfor interactive bumps.
- Match existing kirchDev house style for any new meta files. The reference for CLI structure is
../forgemap. - Keep PRs small and single-concern. The TUI rendering is the riskiest piece — when changing layout, smoke with
bun dist/bin/envprism.mjs tui examples/and decode the ANSI dump if you can't run a TTY. - When writing back to
.envfiles, use the custom serializer. Preserve comments, blank lines, and key order. Round-trip tests are mandatory for any parser/serializer changes. - Out of scope for v0: encryption, remote secrets backends, schema validation, env templating, recursive/monorepo discovery. Keep the architecture open to them but don't ship them.
- Distribution is npm package only, executed via Bun. No multi-platform compiled binaries (
bun build --compile) and no self-upgrade subcommand in v0 — users install withbun add -g envprism. Cross-platform binary distribution can land later if it becomes worth the maintenance cost.