Typed access for your language
penv gen writes a typed file from your schema and remembers where it goes, so your repository is asked once and never again.
Types are generated from your schema, so there is no library to add and nothing to import at
runtime. Two languages ship with the binary, ts and py, and a third is a folder you drop in.
penv gen ts
penv gen pypenv gen with no target lists every target penv can see here, with where each one came from, the
file it writes, the options in effect, and the directories it was detected in.
penv asks where the file goes, once
Two things decide the path and nothing else ever does: an explicit --out, or a remembered answer
in your repository that names an output. Detection never decides. It says whether a target is
relevant to this repository at all, and it offers directories to choose from.
With neither, at a terminal, penv asks:
# PATH
1 apps/web/src/env.ts
2 apps/api/src/env.ts
where should env.ts go? [apps/web/src/env.ts] (Enter, number, path, none):Enter takes the first suggestion, a number takes another off the list, a typed path is used as
written relative to the repository root, and none skips the target. The table appears only when
there is more than one suggestion.
A directory counts as a suggestion when it holds any one of that target's detect files. ts looks
for package.json or tsconfig.json. py looks for pyproject.toml, requirements.txt,
setup.py or Pipfile. penv walks three levels deep, skipping node_modules, dist, build,
target and every directory whose name starts with a dot. Suggestions come shallowest first by
name, with the workspace root last, so Enter in a monorepo lands on a package.
With nobody to ask and no flag, penv writes nothing for that target and reports it skipped, saying
to pass --out <PATH>. That is what a script or a pipeline hits, so pass the flag there.
The answer is remembered in a file you commit
name = "ts"
output = "apps/web/src/env.ts"
[options]
# penv: Property names in the exported object: the environment key, or its camel form. (upper|camel)
key_case = "camel"
# penv: The accessor every key is read through: process.env, import.meta.env or Deno.env.get. (node|vite|deno)
runtime = "node"That is the file Penv Cloud commits. Each knob sits under a comment saying what it changes and what
it takes, so the settings live in the repository rather than in a manual. Edit a value there and the
next penv gen uses it.
penv writes this file for an explicit --out too, and for an answer that happens to equal the
built-in default, because it was still an answer. A folder that says more than penv wrote, or that
carries a # line penv did not write, was written by hand and is never rewritten.
A path outside the repository is refused rather than written, whether it arrived as an absolute path
or as a ...
The options each target takes
penv gen ts --optionsThat prints each knob with its current value, its default, the words it takes and what it changes, then names the file to set it in.
| Target | Option | Values | What it changes |
|---|---|---|---|
ts | key_case | upper, camel | Property names in the exported object. |
ts | runtime | node, vite, deno | The one accessor every key is read through. |
py | pydantic | false, true | Pydantic types for urls and secrets. |
Defaults are upper, node and false. penv suggests a different value when your package implies
one: a vite.config.* suggests vite, a deno.json suggests deno, and a pyproject.toml that
lists pydantic suggests true. It only ever asks where a suggestion and the default disagree, and
with nobody to ask it takes the default silently.
What the generated file looks like
ts reads every key through one accessor and exports a Standard Schema validator beside the typed
object. A key with a default in the schema reads through the accessor with the default applied,
rather than widening to | undefined.
// Generated by penv 1.0.0-alpha.3 from .env.schema. Do not edit.
const read = (key: string): string | undefined => process.env[key];py renders on the standard library alone. With pydantic = true it swaps HttpUrl and
SecretStr back in.
penv never edits your tsconfig.json, your package.json or any other build config. It prints the
import line instead. For ts that is import { env } from "<path>", following your extends chain
and using an alias from your paths map when one is proven to reach the file. For py it is
from <module> import env.
A Python package with a src layout is handled by the target's own layout rule: a package holding
src/billing/__init__.py is offered src/billing/penv_env.py and imports
from billing.penv_env import env.
Check it in a pipeline
penv gen ts --check--check compares what penv would write with what is on disk, and compiles the output when the
toolchain is present. ts looks for tsc in the package's own node_modules/.bin before PATH.
py takes the first of python3, python or py that answers. When neither is there, penv says
which tool it looked for. --check never asks a question, and it reports a skipped target at exit
code 0.
Add a language of your own
A target is a folder holding a target.toml and an env.tmpl template. Drop one into
.penv/targets/go/ and penv gen go works. The binary knows no language by name.
penv looks in three places in order: .penv/targets/<name>/ in the repository, then
~/.penv/targets/<name>/, then the built-in targets. The order is read through rather than
winner-takes-all, so a folder holding only a target.toml inherits the template from the next
place, and a field that file does not set is inherited the same way, key by key inside a table. A
folder holding an env.tmpl and no target.toml overrides nothing and is refused as the typo it
is.
A target receives what penv schema --json prints and nothing else. No values reach a template,
and a template makes no network call.
Do it in order
penv genwith no target, to see which targets apply here.penv gen tsand answer where the file goes.- Paste the import line penv printed into the file that reads your settings.
- Commit
.penv/targets/ts/target.tomlalongside the generated file. penv gen ts --optionsif you want a different accessor or key case, then edit the remembered file and runpenv gen tsagain.- Add
penv gen ts --checkto your pipeline so a schema change that nobody regenerated fails the build.