Coming from cosmiconfig
A cosmiconfig alternative with zero dependencies: import { cosmiconfig } from seniority, graded 186 / 243 by cosmiconfig's own suite (YAML is the gap) — with provenance, so every resolved value can say where it came from, as text, --json data or an agent event.
seniority is a cosmiconfig alternative that also covers what dotenv, rc and
find-up do beside it: one resolution for flags, environment, config files, a
package.json field and defaults, in a fixed order, with no dependencies.
Migrate from cosmiconfig in one import
- import { cosmiconfig } from 'cosmiconfig';
+ import { cosmiconfig } from 'seniority';The root export carries cosmiconfig's surface: cosmiconfig, cosmiconfigSync, Explorer,
ExplorerSync, defaultLoaders, defaultLoadersSync, getDefaultSearchPlaces,
globalConfigSearchPlaces and metaSearchPlaces — all three search strategies, both caches,
$import, and the meta-config merge.
YAML is the one gap. seniority bundles no format parser, so its loadYaml reads the
subset of YAML that is also JSON and refuses the rest by name. Pass
loaders: { '.yaml': yaml.load } and you have cosmiconfig's behaviour, with the parser as
your dependency rather than everyone's.
Is seniority compatible with cosmiconfig?
Not fully, and the number says so. cosmiconfig's own suite, vendored at 10.0.1 and unmodified
apart from the import specifier, runs against seniority beside a control that runs it
against real cosmiconfig:
| passing | rate | |
|---|---|---|
seniority | 186 / 243 | 76.5% |
| cosmiconfig itself (control) | 240 / 243 | 98.8% |
From Compatibility, which npm run compat:page generates from the
oracle's last run; that page is the authority, and it names every case that leaves the gate.
Of the 57 cases not passing, seniority's README and that page account for each: 54 are the
YAML refusal above (the largest block is import.test.ts, whose fixtures are all .yml),
one is the harness reaching for a file path the vendored copy does not have, and two are the
XDG global-directory cases, which register only on Linux and are counted against seniority
on every platform.
Below 100%, seniority is never described as cosmiconfig-compatible; this is a grade.
The same page grades seniority/lilconfig (lilconfig's own suite), seniority/dotenv and
seniority/rc.
What you gain over cosmiconfig
cosmiconfig finds a file. seniority resolves the value, and keeps the receipt:
- Provenance on every value.
resolve()returns each option's winning source and every candidate it beat.explain('region', result)printsregion = "eu-2" from config file ./app.config.jsonwith the unset flag and env candidates beneath it. - The same record for agents.
explanationJson()is that record as--jsondata, andexplanationEvent()is it as an agent event — generated by the code that picked the value, so neither can drift from the text. - An order nobody can rearrange.
flag > env > config file > package.json field > default, exported asORDER. A plugin can add a source between them and can never beat the flag the user typed — so the agent reading your--schemaand the person reading your--helpcan reason about it from the outside. resolveis pure. No filesystem, noprocess.env: the environment is passed in, so it is testable without a process.
When to switch from cosmiconfig
- "Why is this set to that?" is a question your users or agents ask, and nobody can answer it without reading the source.
- You want config, env and flags in one precedence rather than three hand-written ones.
- You do not need YAML, or you are content to pass the parser in.
The API, the discovery rules and the other drop-in paths are on seniority.