seniority

Changelog

Every release of seniority, newest first, from its CHANGELOG.md — what changed and the pull request it came from.

2.0.1

Patch Changes

  • #1548 f190400 Thanks @ofri-peretz! - seniority/lilconfig follows lilconfig 3.1.5 (81 / 81, control 81 / 81): a relative searchFrom and a relative or slash-ended stopDir are resolved against the working directory.

    seniority/dotenv's dotenv run follows dotenv 18.0.10 (198 / 198, control 198 / 198). When the command dies from a signal, the signal is re-raised as before. If that does not end the process, it now exits with 128 + signal a second later instead of hanging. That is the case for a signal Node ignores, such as SIGPIPE (dotenv run -- cmd | head).

2.0.0

Major Changes

  • #1491 5054fb7 Thanks @ofri-peretz! - Requires Node 22.13 or later; Node 18 and 20 are end-of-life. engines.node is now >=22.13.0 (was ^20.19.0 || >=22.13.0), and Node 22.13, 24 and 26 are the versions tested. burgee migrate now warns when a project's engines.node admits a Node below 22.13.0, and its suggested fix is npm pkg set "engines.node=>=22.13.0".

Patch Changes

  • #1475 fca94b2 Thanks @ofri-peretz! - Six fixes.

    • seniority/rc keeps NAME_a=1 whatever order the environment lists NAME_a__b in. The __ nesting walk replaced a value with an object to nest into, so { NAME_a: '1', NAME_a__b: '2' } gave { a: { b: '2' } } and the reverse order gave { a: '1' }. rc's walk stops at a value that is not an object, and drops the deeper key: both orders now give { a: '1' }.
    • seniority/rc returns objects and arrays of its own. The merge stored a nested object or array from defaults or argv by reference, so rc(name, defaults).a.b = 2 changed the caller's defaults. It now copies them, as deep-extend does; a Date or class instance is still carried whole.
    • seniority/rc skips /etc on Windows by default, as rc does. win defaulted to false everywhere, so Windows read /etc/<name>/config and /etc/<name>rc. It now defaults to whether the platform is win32; passing win still decides.
    • seniority/yaml reads an indentation indicator at the top level as js-yaml does. |2 on a document's own block scalar counted from column 0 rather than YAML's -1, so |2\n x read ' x\n' where js-yaml reads ' x\n' (and |1\n x lost its space). A block scalar under a key or a sequence item was already right.
    • seniority/yaml reads an indented % line as a directive, as js-yaml does. A document of one indented line, a space then %x, was refused as stray content and %YAML 1.2\n %x\n---\na as a bad document; the first is now js-yaml's directives end mark is expected and the second reads 'a'.
    • seniority/yaml reports an unclosed multi-line quoted scalar at the end of the input as js-yaml does. k: "unterminated\n was unexpected end of the stream within a double quoted scalar; it is now js-yaml's deficient indentation (2:1).

1.2.1

Patch Changes

  • #1483 3e69fb2 Thanks @ofri-peretz! - The package manifest names its author and publishes with publishConfig { "access": "public", "provenance": true }. burgee's homepage is its landing page, https://burgee.interlace.tools. oxmutate gains keywords, so npm search finds it, and a sideEffects list (its CLI, its worker and its Vitest runner run on load), so a bundler can drop the rest. No code changes.

1.2.0

Minor Changes

  • #1264 e739f18 Thanks @ofri-peretz! - Provenance now gives the line for a value set in the package.json field. The new packageLayer(path, name) in seniority/config (and the root) reads the field named after your program as the layer below the config file, and records the line each of its keys is set on. explain then prints package.json field in /app/package.json:6, not just the file. Pass the parsed manifest as a third argument if you already have it, and the file is read again only for the lines.

    lineOf now reads the top level of the JSON only. It used to return the first "key": anywhere in the file, so a nested key with the same name, written above the top-level one, was cited at the wrong line. A key set twice is cited at the last one, the one JSON.parse keeps.

    burgee: --explain and meta.provenance now give the line for a value set in the package.json field (package.json:5), as they already did for a config file.

Patch Changes

  • #1285 6570ff2 Thanks @ofri-peretz! - seniority/dotenv/cli follows dotenv 18.0.9: dotenv run reads an empty DOTENV_PATH or DOTENV_CONFIG_PATH as unset and loads ./.env, where it used to read the empty string as a path and stop with EISDIR. dotenv 18.0.9's own suite grades seniority/dotenv 197 / 197, level with dotenv itself.

1.1.1

Patch Changes

  • #1258 e3b3a41 Thanks @ofri-peretz! - check reads a .json plugin file as data. It handed every file to import(), and Node refuses a .json module without an import attribute (ERR_IMPORT_ATTRIBUTE_MISSING), so a plugin written as JSON got Node's message and no verdict, from the host's own check and through burgee check, which runs it.

  • #1266 02e6f9f Thanks @ofri-peretz! - The family plugin schema (schema.json, identical in every plugin host) now describes formats, burgee's output-format key. This package reads nothing new, and a plugin carrying formats registers here as before.

  • #1252 efc994e Thanks @ofri-peretz! - The README names the Node builtin the package builds on, and says what the package adds over it: Intl.Segmenter and util.stripVTControlCharacters (linegauge), process.loadEnvFile and util.parseEnv (seniority), node:child_process (bellpull), process.on(signal) (closeout). The README's opening names the package and its incumbents only, and every benchmark figure in it links to where it is measured.

1.1.0

Minor Changes

  • #1203 801875d Thanks @ofri-peretz! - The precedence order is now exported as a truth table. TRUTH_TABLE has 32 rows, one for each combination of the five sources (flag, env, config, package, default). Each row lists the sources that set a value and names the one that wins. It is truthTable(ORDER), so it always matches the order resolve uses, and the new docs page The truth table is generated from it.

    Provenance through extends now names the right file. When a config extends another, a value set only by the parent used to be credited to the extending file, which does not contain it. The loaded layer now has origins: for each key, the nearest file in the chain that set it, and that file's line. resolve, explain and --json provenance cite that file and line. A file that extends nothing loads the same as before.

    burgee: --explain and meta.provenance now give the line for a value set in a config file (app.config.json:3). They also name the file in an extends chain that set it, not the whole chain joined into one location.

1.0.12

Patch Changes

  • #1186 af24328 Thanks @ofri-peretz! - Published declarations now keep each doc comment's first paragraph, its code examples and its @param, @returns, @throws, @example, @deprecated, @default and @see tags, and drop the longer design notes below them, so the tarballs are smaller (burgee by 5.7% gzipped, bellpull and closeout by about a fifth). Types are unchanged, and hover text in an editor keeps its summary. burgee's README also names burgee/program-schema.json, the JSON Schema every --schema document validates against.

1.0.11

Patch Changes

  • #1160 0b381a0 Thanks @ofri-peretz! - Export ./package.json. require('<package>/package.json') threw ERR_PACKAGE_PATH_NOT_EXPORTED, which broke tools that read a dependency's manifest that way: bundlers, license scanners and version checks.

1.0.10

Patch Changes

  • #1138 6b90ffe Thanks @ofri-peretz! - seniority/dotenv follows dotenv 18.0.8: parse(src, { fast: true }) unquotes a segment that ends at a U+2028 or U+2029 inside an unquoted value, as the regex parser does, and populate takes and returns dotenv's own DotenvPopulateInput / DotenvPopulateOutput, now string | undefined because an undefined input is copied as given. dotenv 18.0.8's own suite grades it 193 / 193, level with dotenv itself. seniority/lilconfig follows lilconfig 3.1.4: its JSON loader drops a leading byte-order mark, so a BOM-prefixed package.json or rc file loads instead of throwing. lilconfig 3.1.4's suite grades it 77 / 77.

1.0.9

Patch Changes

  • #1130 c05c86e Thanks @ofri-peretz! - Every drop-in subpath now type-checks under moduleResolution: node10, and under module: commonjs with no resolution set, which is TypeScript's default there. import … from 'burgee/commander' used to fail with TS2307 in such a project, although require() of the same specifier worked. Each package now declares types and a typesVersions map, derived from exports, with one key per typed subpath pointing at the file exports already names. exports is unchanged, so node16, nodenext and bundler read exactly what they read before. controlroom/ink's declarations no longer name the untyped react-reconciler, which was TS7016 for consumers with skipLibCheck: false.

1.0.8

Patch Changes

  • #1075 48ccbaf Thanks @ofri-peretz! - seniority/dotenv/cli on Windows: dotenv run now refuses an argument with a line break or NUL in it, and an environment with a variable name ending in ^, before anything starts. It reports dotenv: … and exits 1, because cmd.exe cannot carry those. It also escapes : for cmd, so %name:…% can no longer expand a variable into the command line. Each closes a way a crafted argument could reach cmd.exe live. Arguments that are not refused reach the program exactly as before.

1.0.7

Patch Changes

  • #1045 c4fd9a3 Thanks @ofri-peretz! - The family plugin schema (schema.json, identical in every plugin host) now describes locales, burgee's locale-pack key. A plugin carrying locales validates against the shared schema as it does against burgee/locale-pack-schema.json, which lists every message id. This package reads nothing new, and a plugin carrying locales registers here as before.

1.0.6

Patch Changes

  • #1022 ecb0a00 Thanks @ofri-peretz! - The tarball no longer carries declaration files that no exports entry reaches. TypeScript could never open them, because a deep import is refused by exports. Every entry's types are unchanged. The ten packages drop 71 files and 118,312 bytes between them, burgee 35 files and 76,504 bytes, which takes its tarball from 251,814 to 225,278 bytes gzipped.

1.0.5

Patch Changes

  • #1005 079250e Thanks @ofri-peretz! - seniority/find-up is now find-up 8's surface, graded by find-up 8.0.0's own suite: 104 of 104, level with find-up itself. It adds a matcher function with findUpStop, findDown and findDownSync, and locate-path's type and allowSymlinks; cwd and stopAt take a URL. Three behaviours follow find-up rather than seniority's resolver walk: cwd is optional and defaults to the process's working directory, a name matches a file unless type says otherwise, and findUpMultiple returns the first matching name per directory. burgee migrate now rewrites find-up to seniority/find-up.

  • #1001 6f74a64 Thanks @ofri-peretz! - The README no longer says every path is graded: seniority/find-up has no vendored find-up suite yet, so it now says "not graded yet" until it has a number.

1.0.4

Patch Changes

  • #977 2e2030e Thanks @ofri-peretz! - seniority/dotenv follows dotenv 18.0.7: config() treats an option passed as undefined as not passed, so { path: undefined, quiet: undefined } keeps the DOTENV_PATH / DOTENV_QUIET (and DOTENV_CONFIG_*) defaults instead of overriding them, and dotenv run (seniority/dotenv/cli) honours a DOTENV_QUIET set in the .env it loads when neither a flag nor the starting environment said otherwise. dotenv 18.0.7's own suite grades it 191 / 191, level with dotenv itself.

1.0.3

Patch Changes

  • #938 9babb6d Thanks @ofri-peretz! - npm description and README: the description says what the package does, then its dependency fact in one of two wordings ("Zero dependencies." or "No dependency outside the burgee family."), then its drop-in paths. The README's family section states how the family is built — the leaves, one-way dependencies, nothing from outside the family, trusted publishing with provenance — and its table column is "Migrates from". No API, compatibility grade or benchmark changes.

1.0.2

Patch Changes

  • #917 735f0f6 Thanks @ofri-peretz! - README: the opening paragraph says what the package does first. The drop-in path follows it as one line, naming the incumbent and npx burgee migrate, and every compatibility row and benchmark is unchanged.

1.0.1

Patch Changes

1.0.0

Major Changes

  • #862 6aab9c5 Thanks @ofri-peretz! - seniority 1.0.0. The spec is fully built, every drop-in passes its incumbent's own suite at the latest release level with the incumbent itself (dotenv 18.0.6 181 / 181, lilconfig 3.1.3 77 / 77, rc 1.2.8 1 / 1, cosmiconfig 10.0.1 242 / 243 against its own 242), and coverage is 100% on all four measures (D-20261008-seniority-1-0-evidence). The published entries, the plugin schema.json and the seniority bin become a semver contract. No API changed in this release.

Patch Changes

  • #848 6ae533e Thanks @ofri-peretz! - The family schema.json describes what five values look like. contract is 1, and burgee refuses a plugin that declares none. A caique widget's static(spec) gets { kind, message, ...sample.done }. A seniority rank sits between the built-in layers at flag 0, environment 10, config file 20, package.json 30 and default 40. A burgee hook's filter is { command: RegExp }, now with command required, so the schema and the host refuse the same filters.

  • #857 d80b2d0 Thanks @ofri-peretz! - The family schema.json closes a bellpull resolver's when with additionalProperties: false, so the schema and bellpull's own validation refuse the same unknown keys.

  • #848 6ae533e Thanks @ofri-peretz! - Each README now shows a whole plugin as a default export, the command that checks it (from an install, and from a clone where dist/ has to be built first), and the report check prints when the plugin passes. caique says what a widget's static(spec) receives. seniority gives the rank for each slot between the built-in layers. bellpull says that {VAR} is the only substitution a path gets.

  • #847 6d3eda2 Thanks @ofri-peretz! - seniority/dotenv follows dotenv 18.0.6: populate reads override and debug the way dotenv's parseBoolean does, so the string 'false' (or '0', 'no', 'off', '') turns them off instead of on. dotenv 18.0.6's own suite grades it 181 / 181, level with dotenv itself.

0.8.0

Minor Changes

  • #828 8118d5c Thanks @ofri-peretz! - seniority/dotenv now follows dotenv 18, graded by dotenv 18.0.5's own suite at 179 / 179, level with dotenv itself (it was 106 / 141 against 17.4.2). config() takes its defaults from DOTENV_PATH, DOTENV_ENCODING, DOTENV_QUIET, DOTENV_DEBUG, DOTENV_OVERRIDE and DOTENV_FAST, or their older DOTENV_CONFIG_* names. It reports ◇ injected env (n) from <paths> on console.error unless quiet, and its debug lines behind ┆ on console.log. parse(src, { fast: true }) is dotenv's character scanner, and the regular-expression parser now expands \n in a value that opens with a double quote even when the quote never closes, as dotenv does. configDotenv is exported. Two entries are new: seniority/dotenv/config (import 'dotenv/config', quiet unless asked) and seniority/dotenv/cli (dotenv run, with signal forwarding). .env.vault, DOTENV_KEY and decrypt are not built, because dotenv 18 removed them (D-20261001-seniority-dotenv-vault).

    burgee migrate now rewrites dotenv to seniority/dotenv, and dotenv/config and dotenv/config.js to seniority/dotenv/config, on dotenv 18. A project on dotenv 17 is left alone and listed under offMajor: 17's own suite grades the drop-in 107 / 141, because the façade speaks 18.

0.7.2

Patch Changes

  • #816 5230016 Thanks @ofri-peretz! - controlroom hosts plugins (R10): keymaps and panes register through controlroom/plugin's register() against the family schema, a screen takes either by name, and controlroom check <plugin-file> reports what a plugin contributes. The family schema every host ships gains the keymaps and panes definitions.

0.7.1

Patch Changes

  • #786 d810ac0 Thanks @ofri-peretz! - The cosmiconfig drop-in's global search now ends in the directory cosmiconfig uses. That directory comes from env-paths: $XDG_CONFIG_HOME/<name> on Linux, falling back to ~/.config/<name> when the variable is unset or empty, %APPDATA%\<name>\Config on Windows, and ~/Library/Preferences/<name> on macOS. Before this fix, the drop-in built the directory from the home directory alone. A Linux user with a non-default XDG_CONFIG_HOME who moved to seniority with burgee migrate silently lost their global config. The environment is read when the search runs, through the package's one runtime seam, and only when globalConfigDir is not passed. On Linux, cosmiconfig 10.0.1's own suite now grades seniority at 242 / 243, the same as cosmiconfig itself (D-20260930-seniority-xdg-config-home).

0.7.0

Minor Changes

  • #778 24300f4 Thanks @ofri-peretz! - seniority's cosmiconfig drop-in now reads YAML the way cosmiconfig does. .yaml, .yml and extensionless rc files go through seniority/yaml, the package's own parser, which is loaded the first time a YAML file is read and never before. The loader used to read only the JSON subset of YAML and throw a LoaderError (no YAML parser for <file>) for the rest. It now returns what js-yaml 5 returns. A malformed file throws cosmiconfig's own message, YAML Error in <file>: followed by the reason and (line:column). cosmiconfig 10.0.1's own suite grades seniority at 240 / 243, up from 186 / 243, level with the control's 240 / 243.

    burgee migrate now rewrites cosmiconfig to seniority, because the row is level. A file that imports a name seniority does not export, such as the type LoaderSync, is refused as unknown-export and stays on cosmiconfig. On Linux, the global config directory is still resolved from the home directory rather than from XDG_CONFIG_HOME (D-20260930-seniority-yaml).

  • #776 8ba7c33 Thanks @ofri-peretz! - New entry, seniority/yaml: a YAML parser with no dependency. parse(text) returns what js-yaml 5's load returns for the part of YAML a configuration file is written in — block and flow mappings and sequences, plain, quoted and block scalars, comments, document markers, anchors and aliases, and the core schema (null, booleans, integers, floats, strings, and the !!str / !!int / … tags). Malformed input throws a YAMLException with js-yaml's reason and its 1-based (line:column). Every expectation in its tests is also checked against js-yaml on every run. Nothing else in the package imports it, so a program that reads no YAML carries none of it. Pass it to discover as a loader to read YAML config files: loaders: { '.yaml': (_path, text) => parse(text) } with extensions naming .yaml (D-20260930-seniority-yaml).

0.6.5

Patch Changes

  • #752 22e6dae Thanks @ofri-peretz! - A plugin that declares contract: 0 or a negative contract is now refused with E_PLUGIN_CONTRACT, as schema.json's minimum of 1 always said. These hosts checked only that a contract was not newer than the one they know, so 0 and below registered.

  • #749 bc59cab Thanks @ofri-peretz! - At an equal rank the plugin registered later now wins, as the docs and register() always said. sources() returned tied plugins oldest first and resolve takes the first candidate with a value, so the earlier registration won the tie.

0.6.4

Patch Changes

  • #674 e9f45d8 Thanks @ofri-peretz! - README: family header, badges, install, migrating, the family table.

    Every package README now opens the same way — lockup, tagline, one badge row in one order (npm version, downloads, Quality Gate, the package's own coverage, OpenSSF Scorecard, unpacked size, dependencies, types, Node, licence, npm provenance), a row of compatibility badges read from the graded baseline — and carries the same sections in the same order: Install for npm, pnpm, yarn and bun, Quick start, Migrating as a before/after diff, Compatibility, Benchmarks, For agents, API, and a generated table of the nine packages. Links are absolute, so they work on npm as well as GitHub.

  • #731 bc68493 Thanks @ofri-peretz! - seniority/cosmiconfig now matches cosmiconfig 10.0.1 in three places where it did not.

    • stopDir: '' searches the start directory alone, as upstream's truthiness check does. Before, an empty stopDir switched the search to global and walked up to the working directory and then into the global config directory.
    • packageProp walks a path the way upstream does. A path through a string reads the string's own properties, so 'name.length' is a number. A path through a null, such as "foo": null under packageProp: 'foo.bar', throws the TypeError upstream throws, annotated with the file. Before, both answered "not found", and the search moved on to the next file.
    • A start directory that cannot be stated for any reason but absence rejects the search with the stat error, as upstream's isDirectory does. Before, every such failure read as "no config here". Which paths fail that way is the platform's call: on Linux and macOS, search('<file>/sub') rejects with ENOTDIR and a directory the process may not enter with EACCES; Windows reports <file>\sub as not found, so there it is still "no config here", as it is upstream.

    loadJson no longer wraps a non-Error in an Error, because JSON.parse of a string throws nothing else.

  • #732 ee6780b Thanks @ofri-peretz! - Code no input could reach is removed. There is no behaviour change.

    • seniority check loses a "(replaces …)" helper it never called, and a map that returned each source name unchanged. It loads one plugin into an emptied registry, so there is nothing for a source to replace. The header now says that.
    • seniority/lilconfig no longer writes dirname(p) || sep, because Node's dirname never returns ''.
    • seniority/dotenv's parser no longer checks that a match has a key, because the key group in its pattern is not optional.
    • seniority/rc's comment stripper reads characters with charAt, so there is no fallback for an index that is always in range.
    • The JSON loader behind seniority/config quotes the parser's own message without first checking that it threw an Error, because JSON.parse of a string throws nothing else.

0.6.3

Patch Changes

  • #627 4a629a9 Thanks @ofri-peretz! - Lint with every published Interlace ESLint plugin, and fix what the upgrade surfaced.

    • caique: the inquirer theme merge skips __proto__, constructor and prototype keys, so a theme object cannot swap the merged object's prototype.
    • burgee: last-element reads use .at(-1).
    • burgee, closeout, flagstaff, roundel: helpers that capture nothing from their enclosing function move to module scope.
    • seniority: suppression comments name the no-dynamic-require rule that now reports the config loader's dynamic require.

    No public API or output changes.

0.6.2

Patch Changes

  • #604 0e7b1e8 Thanks @ofri-peretz! - Each README links to its migration guides under the docs link: "Migrating from: chalk", "ora · log-update · boxen · cli-table3", and so on. That puts a path from the npm page to the guide for the library you are replacing. No code changes.

0.6.1

Patch Changes

  • #588 073037a Thanks @ofri-peretz! - READMEs and package descriptions now match what each drop-in path is graded at. bellpull names bellpull/node-which as the drop-in for npm which (5 / 5) and no longer lists execa as a drop-in. paratext's ansi-escapes row is 4 / 4, with the CSI half implemented. seniority's rc row is 1 / 1, dotenv's config() defaults to process.env, and lilconfig is 77 / 77. linegauge documents ambiguousIsNarrow and strip as shipped. No code changes.

0.6.0

Minor Changes

  • #486 f08ff58 Thanks @ofri-peretz! - The seniority/dotenv and seniority/rc drop-ins now read the process by default, like the packages they replace. config() with no arguments populates process.env from ./.env; rc(name) reads the process environment. config() also accepts a URL or ~/ path, calls fs/os in a way test stubs can intercept, and returns parsed alongside any error, matching dotenv 17. Graded by each incumbent's own test suite: dotenv 80 → 106 of 141, rc 0 → 1 of 1. seniority's resolver still never reads the process itself.

Patch Changes

  • #474 1955419 Thanks @ofri-peretz! - burgee plugins can hook two more stages. parse runs before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin maps d to deploy. shutdown runs once as the program exits, whether the command succeeded or failed. The family schema.json shipped in every package now describes both stages.

  • #522 f4be6a8 Thanks @ofri-peretz! - require('bellpull/cross-spawn'), require('flagstaff/cli-table3'), require('burgee/yargs'), require('seniority/dotenv') and require('seniority/rc') now return what the incumbent's require() does — the function, the class, the factory, the object — instead of an ES module namespace. Each exports its default as 'module.exports', which is what Node hands a CommonJS caller, and which yargs' own entry already does. const spawn = require('…'); spawn(…) threw before.

0.5.1

Patch Changes

  • #508 1aae1e2 Thanks @ofri-peretz! - <package> --help and --version answer instead of crashing. The bin took its first argument as the plugin file to import, so roundel --help failed with Cannot find module '…/--help' and exit 1. -h/--help now print usage and exit 0, -V/--version print the version and exit 0, and any other flag where the plugin file belongs is a usage error, exit 2.

0.5.0

Minor Changes

  • #507 b8e97dc Thanks @ofri-peretz! - Runs on Node 20 and 22, not just 24+: engines.node is now ^20.19.0 || >=22.13.0. Those are the first releases where require(esm) loads without a warning, so the CommonJS require() path keeps working. Every package's test suite runs on exactly 20.19.0 and 22.13.0, on Linux, macOS and Windows. caique's prompts no longer call Promise.withResolvers, which Node 20 doesn't have.

Patch Changes

  • #505 9800b43 Thanks @ofri-peretz! - Docs: the Benchmarks section's weight ceiling is re-measured against a fresh install of each incumbent's latest release (cosmiconfig 10.0.1, slice-ansi 9.0.1, which 7.0.0, dotenv 18.0.3, …) instead of the copies hoisted in this workspace, and names incumbents that were measured but left out of the ceiling as exactly that.

0.4.3

Patch Changes

  • #494 f7f6d4b Thanks @ofri-peretz! - Each package's homepage and README docs link now point at its own documentation site, https://<package>.interlace.tools, instead of a page on burgee's site. The old burgee.interlace.tools/docs/packages/<package> URLs answer with a 301 to the new host, so nothing already linked breaks. closeout's README override example also resolves to the current release again (npm:closeout@^0.4; the 0.4.0 release left it at ^0.3).

0.4.2

Patch Changes

  • #465 acf98f3 Thanks @ofri-peretz! - Each README now opens with the incumbent it replaces and the agent surface it serves (--json, an agent event, or a static projection), so npm shows both above the fold. README text only; no code changed.

0.4.1

Patch Changes

  • #454 b4584e7 Thanks @ofri-peretz! - Every package's npm homepage now points at its page on the docs site, https://burgee.interlace.tools/docs/packages/<name>, and each README links it under the header. The keywords add what people and models search for: burgee gains cli-framework, argument-parser, subcommands, json-schema, mcp-server, model-context-protocol, ai-agent, llm, shell-completion, typescript, zero-dependency, commander-alternative and yargs-alternative; the other eight gain agent, ai-agent, non-tty, json and zero-dependency where the package does that — zero-dependency only on the six that install nothing at all.

    burgee's README gains a short FAQ (commander alternative, agent use, MCP, dependencies) and states the compatibility counts the oracle holds — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' — where it had said 1,215 and 1,185. caique's README no longer calls a released package pre-release.

  • #442 bdaf364 Thanks @ofri-peretz! - Every package now lists plugin, plugins and extensible in its npm keywords, because every package takes plugins through one shared contract.

    A plugin is a plain object, validated against the schema.json that ships in every package, and checked with the package's own check command. Each package reads its own key and ignores the rest, so one object can extend any subset of the family. The plugins page has a nine-layer example that every package's check accepts in CI.

  • #435 7888524 Thanks @ofri-peretz! - schema.json now describes every plugin host in the family.

    The one schema each package ships as its plugin contract used to cover only four hosts: roundel's tokens, flagstaff's glyphs, spinners, borders and components, paratext's capabilities, and linegauge's widths. Five hosts validated their keys in their own code, but the file an author (or a model) writes against said nothing about them. It now describes all of them:

    • bellpull resolvers, including the absolute-path rule on paths
    • caique widgets
    • closeout handlers, including the phases a plugin may use
    • seniority sources, including the rank bounds
    • burgee commands, hooks and enforce

    Where the schema can express a rule, it gives the same verdict as the host's own validator, and a test holds the two together. Function-valued fields (static, run, read, handler) are described and required, but not typed, because JSON Schema can't say "function".

    flagstaff now validates a plugin against only its own keys, not the whole family schema. It no longer refuses a plugin over another host's key, which lets one plugin object contribute to several hosts. Its entry points are also 4.7–5.9 KB lighter for it.

  • #445 dac303e Thanks @ofri-peretz! - Every package now declares sideEffects truthfully, so bundlers can drop what you don't import.

    Six packages declared nothing, so no bundler could drop any of their modules. A named import from the root now bundles to the same bytes as the same import from its subpath:

    importbeforeafter
    import { explain } from 'seniority'2,939 B1,067 B
    import { decide } from 'caique'1,235 B734 B
    import { strip } from 'linegauge'1,102 B940 B
    import { once } from 'closeout'353 B235 B

    flagstaff and roundel used to declare false, but each ships a check command whose file runs when loaded. Each now lists that file, which is the true statement. paratext also lists the two modules that register its built-in capabilities when they load.

0.4.0

Minor Changes

  • #421 db3c59e Thanks @ofri-peretz! - Every plugin host has a check command.

    npx linegauge check ./my-widths.mjs
    npx burgee check ./my-plugin.mjs --json

    PRINCIPLES 7 asks three things of an extension surface: the plugin is data validated against one published schema, there is a check command that shows it every way it can be seen, and the bar is measured. The first was built in all nine hosts; the second existed in flagstaff alone. So an author writing a plugin for any other host found out what it did by shipping it into a program — and a surface nobody can check is a surface nobody outside this repository can write against.

    Each command validates, registers, and shows what the host does with the plugin, in the host's own terms: linegauge measures each code point before and after the override, paratext shows a capability's encode and its fallback, roundel each token and what it replaced, caique each widget's static projection rendered with its own sample. burgee's returns a document rather than printing one, so burgee check --json is the form an agent that just wrote a plugin reads.

    They share one contract with the author, held identically across all nine:

    • a readable report, contribution by contribution, with ok as the last line;
    • a refusal with a code from the family's vocabulary and a fix, exit 1;
    • E_NO_CONTRIBUTION for a plugin that contributes nothing to this host — the schema allows unknown keys so one object registers everywhere, which makes a misspelled key silent, and this is how that typo tells on itself;
    • exit 2 with no file.

    Each host also gains an eval case measuring the one-turn claim, proved to discriminate before it was committed: green against a correct plugin, red against the same plugin with one field broken.

  • #418 c0fa8a3 Thanks @ofri-peretz! - explain moves from seniority/precedence to seniority/explain.

    A re-export is not free across a package boundary. explain was exported from precedence.ts, so every program that resolved a configuration loaded explain.js whether or not anything ever explained one — 1,018 bundled bytes and one more module on the startup path for the branch taken when a user asks why did this option get that value.

    import { explain } from 'seniority' is unchanged: the root barrel still exports it, from its new home. Only seniority/precedence stops re-exporting it. burgee/config re-exports it the same way it always did, and burgee's engine loads it behind an await import('seniority/explain') on the --explain branch, which is now the only thing that pays for it.

    Measured on burgee's core entry: 28,637 → 27,552 bundled bytes, and 22 → 21 modules for import 'burgee'.

Patch Changes

  • #430 4d1b2b3 Thanks @ofri-peretz! - check now reports every refusal with its code and its fix, wherever it was raised.

    Some plugin files register themselves on import: they call register() at the top of the module and export the result. Until now, when such a file was refused, the error was thrown inside check's import(), before the only try that turns a PluginError into E_PLUGIN_SCHEMA: … plus a fix: line. The author got the bare message on stderr, with no code and no fix. Now the whole of check runs inside that one handler, so every refusal comes out the same way on every host.

0.3.1

Patch Changes

  • #386 5a85175 Thanks @ofri-peretz! - The Unicode segmenter is built on first use rather than at import, and both packages now strip comments from what they publish.

    new Intl.Segmenter() loads ICU's grapheme-break data. Two were constructed at module scope, and almost nothing paid for them: every caller takes the ASCII fast path first, so a run of printable ASCII — a help screen, a flag name, a path — never reaches segment(). segmenter is now a function; the two call sites become segmenter().

    linegauge and seniority were also the two published packages whose build never ran strip-comments at all. Unpacked: linegauge 83,538 → 56,148 and seniority 193,682 → 139,793.

    Together these take import 'burgee' from 56.87 ms to 44.39 ms, medians of seven.

0.3.0

Minor Changes

  • #379 955b979 Thanks @ofri-peretz! - seniority/lilconfig and seniority/rc — two new drop-in subpaths, and seniority/dotenv grows the default export its incumbent has.

    seniority/lilconfig is lilconfig 3.1.3's surface: lilconfig, lilconfigSync, defaultLoaders, defaultLoadersSync, and no fifth runtime export, because lilconfig's own suite compares the module's keys against cosmiconfig's. Graded at 67 / 77 against that suite, up from 0 — the same number its control scores, so no case in it now separates the two. The zero was not a missing feature: the row had been pointed at the package root, which presents cosmiconfig's surface and answers lilconfigSync is not a function seventy-seven times.

    seniority/rc is rc 1.2.8's merge with none of its four dependencies: the file stack in rc's own order, __ nesting for environment keys, JSON-with-comments, deep-extend's merge, and configs / config reporting which files were read. INI is refused by name with the argument that would parse it, the way YAML already is. Its environment arrives as an argument rather than off the process, which is the one divergence and the reason its compat row stays at 0 / 1.

    seniority/dotenv now has a default export carrying config, parse and populate, so a CJS caller require()ing it gets the same mutable object require('dotenv') gives — which is what dotenv's own suite stubs. populate also matches 17.4.2 more closely: it validates parsed (not processEnv), returns the keys it actually set, and logs under debug. The row moves 74 / 141 → 80 / 141.

0.2.0

Minor Changes

  • #316 c8acb28 Thanks @ofri-peretz! - seniority to 1.0's requirement set: cosmiconfig's surface, measured at 186 / 241.

    The root export now carries cosmiconfig's own API — cosmiconfig, cosmiconfigSync, Explorer, ExplorerSync, defaultLoaders, defaultLoadersSync, the search-place and loader tables, decodeFileContent and getPropertyByPath — with all three search strategies, both caches, $import with mergeImportArrays, and the meta-config merge. Graded by cosmiconfig 10.0.1's own suite, run unmodified, it goes from 3 / 241 (1.2%) to 186 / 241 (77.2%) against a control of 240 / 241.

    Every one of the 55 cases it does not pass is one of two named divergences: 54 are the absent YAML parser — this package bundles no format parser, so loadYaml reads the JSON subset of YAML and refuses the rest with an error naming the loaders option that supplies one — and one is a test-harness file path. None is a difference in how a config is found, merged or reported.

    Two new compatibility subpaths:

    • seniority/dotenv — dotenv 17's parse and populate, grammar included. config takes the environment it populates as processEnv rather than reaching for process.env, so nothing in this package touches the process.
    • seniority/find-up — findUp, findUpSync, findUpMultiple, findUpMultipleSync over a bounded, symlink-cycle-safe upward walk, in place of four packages.

    Also new on the root export, all additive:

    • explanation() — --explain as a record, with explain()'s text now literally a rendering of it, alongside explanationJson() and explanationEvent().
    • search() / searchAll() — the walk, bounded by stopAt, a depth limit and the filesystem root.
    • loadPath(), loaderFor(), LoaderError — four builtin loaders, injected loaders for every other format, and a usage-class error naming the extension and the option.
    • validate() / check() — a violation reported with its provenance: `out` must be a string; `./mytool.config.js:3` set it to `4`.
    • provenance.line, recorded per key for JSON config layers, and discover's loaders, extensions, upward and stopAt options.

    No behaviour of the existing API changes, and the package still declares zero dependencies.

  • #294 3f92a60 Thanks @ofri-peretz! - Host sources, the plugin key, at seniority/plugin — a plugin adds a resolution source (a vault, a CI variable set, a remote config) and --explain names it as the provenance of the value it won.

    Source is now an open union, so this is additive rather than a type break, and the one code change it costs is describe()'s default branch: an unknown source renders itself from the source and location it declared, which is how a plugin explains itself without seniority knowing its name.

    ORDER is exported and is the single declaration the union and the new RANK are both generated from — the design's array and the shipped union had disagreed (project/home/pkg against config/package), and a plugin must not register against two spellings. The shipped five win, because provenance.source is a value users already read and because project versus home was one kind with two locations, which location already names precisely.

    A plugin's rank slots its source between two built-ins and is refused outside (flag, default): it can never beat the flag the user typed, nor sink below the declared default. The order stays the fixed thing it claims to be.

Patch Changes

  • #316 c8acb28 Thanks @ofri-peretz! - mergeAll refuses prototype as well as __proto__ and constructor, and the guard is three comparisons rather than a Set lookup.

    The guard existed and had no test — it was written, and believed. CodeQL's js/prototype-polluting-function could not see it through the Set binding and blocked a merge on it, which is a fair complaint about a security guard: one a reader has to follow a binding to find is one a reviewer will miss too.

    What it actually prevents, measured rather than assumed. A first attempt at the test asserted ({}).polluted === undefined after merging a __proto__ key and passed with the guard deleted — target['__proto__'] = v goes through the setter and swaps that object's prototype; it does not write Object.prototype. The damage is narrower and quieter: the config object handed back to the caller silently inherits whatever the file said, so config.isAdmin can answer for a key no file set at the top level. cosmiconfig-util.test.ts now asserts that, and four of its six cases go red when the guard is removed.

  • #339 f295630 Thanks @ofri-peretz! - schema.json constrains token names, because it was promising something no host honours.

    tokens was described as any name to a #rrggbb colour. roundel's validate() accepts ten semantic names — error, warn, ok, hint, muted, command, flag, value, heading, ground — and throws on everything else. So a plugin author doing exactly what their own E_PLUGIN_SCHEMA error tells them, comparing their object against roundel/schema.json, got a green from the schema and "accent" is not a token from register(). Measured 2026-09-16 with { accent: '#336699' }.

    The schema now carries propertyNames.enum, and scripts/plugin-contract-lock.test.ts pins the enum and the runtime set to each other from both sides, so neither can grow a name the other does not know.

    Every host ships a byte-identical copy of this file (plugin-schema-lock.test.ts asserts it), which is why nine packages are listed. Only the key roundel owns is constrained: describing widgets, handlers, sources, resolvers or commands in a file all eight hosts share is what made flagstaff start validating caique's key last time (PluginError: plugin.widgets.later: expected object, got boolean), and those stay in plugin-schema-lock's UNDESCRIBED list with that reason.

    linegauge is in the list for a different change: ceilings.json's R9 block now records the bar as D1's tree-inclusive ceiling — 83,538 against 170,342, a ratio of 0.4904 — and keeps the superseded get-east-asian-width bar beside it with the count of entries that cleared it.

0.1.0

Minor Changes

  • #219 ac7b9e5 Thanks @ofri-peretz! - seniority resolves flags, env, config and defaults — with provenance

    The package existed as a reserved name exporting a string constant. It now does the job its description has always claimed.

    flag  >  env  >  config file  >  package.json field  >  default

    resolve(specs, layers) returns the values, the provenance of each, and every candidate that lost — so explain(name, resolution) can print the winning source and the ones it beat, generated by the same code that picked the value. A --explain built any other way can drift from the truth; this one cannot.

    resolve is pure: layers in, values out, no filesystem and no process.env. discover is the half that touches the disk and is a separate import for that reason — searching NAME_CONFIG, ./name.config.{json,mjs,js,cjs} and $XDG_CONFIG_HOME/name/config.json, following extends with deep merge and rejecting cycles with the chain that formed them.

    The code is burgee's, moved down a layer where it belongs: precedence.ts and config.ts with their 28 tests, which passed unchanged. Its OptionSpec here declares only the three fields resolution reads, so any program's richer option type satisfies it structurally — no adapter, no import, and no dependency pointing back up the stack.

    Zero dependencies; Node builtins only.

On this page