This is a compact, self-contained guide for STAN assistants to use @karmaniverous/get-dotenv effectively (library + CLI host + plugins) without consulting type definition files or other project documentation.
get-dotenv composes an environment (ProcessEnv) from multiple sources deterministically, expands references recursively, optionally applies dynamic variables, and then lets you (a) use the final map programmatically, (b) run commands under it via a cross-platform CLI, or (c) build your own plugin-based CLI host that resolves env once per invocation.
Key idea: treat the “resolved dotenv context” (ctx.dotenv) as the source of truth, and do not rely on process.env being mutated unless you explicitly enable it.
import()).import { getDotenv } from '@karmaniverous/get-dotenv'import { createCli } from '@karmaniverous/get-dotenv/cli'import { GetDotenvCli, definePlugin } from '@karmaniverous/get-dotenv/cliHost'import { cmdPlugin, batchPlugin, awsPlugin, awsWhoamiPlugin, initPlugin } from '@karmaniverous/get-dotenv/plugins'import { resolveGetDotenvConfigSources } from '@karmaniverous/get-dotenv/config'import { overlayEnv } from '@karmaniverous/get-dotenv/env/overlay'This guide is split into focused topics:
createCli)Use this section when you need a “what do I import?” answer quickly.
@karmaniverous/get-dotenv):
getDotenv, defineDynamic, defineGetDotenvConfigbaseRootOptionDefaultsdotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnveditDotenvText, editDotenvFile (format-preserving)traceChildEnv, redactDisplay, redactObject, maybeWarnEntropyinterpolateDeep, writeDotenvFile, defaultsDeep, tokenize, applyIncludeExclude, requireString, assertByteLimit, silentLogger, toNumber, parseFiniteNumber, parsePositiveInt, parseNonNegativeInt@karmaniverous/get-dotenv/cli):
createCli({ alias, branding?, compose?, rootOptionDefaults?, rootOptionVisibility? }) -> (argv?) => Promise<void>@karmaniverous/get-dotenv/cliHost):
GetDotenvClidefinePlugin (returns a plugin with readConfig and createPluginDynamicOption)runCommand, runCommandResult, shouldCapture, buildSpawnEnv, ensureForcereadMergedOptionsresolveCommand, resolveShell, defineScriptsgetRootCommand, composeNestedEnv, maybePreserveNodeEvalArgv@karmaniverous/get-dotenv/config):
resolveGetDotenvConfigSources(...) and validation helpersThis is a compact, self-contained guide for STAN assistants to use @karmaniverous/get-dotenv effectively (library + CLI host + plugins) without consulting type definition files or other project documentation.
get-dotenv composes an environment (ProcessEnv) from multiple sources deterministically, expands references recursively, optionally applies dynamic variables, and then lets you (a) use the final map programmatically, (b) run commands under it via a cross-platform CLI, or (c) build your own plugin-based CLI host that resolves env once per invocation.
Key idea: treat the “resolved dotenv context” (ctx.dotenv) as the source of truth, and do not rely on process.env being mutated unless you explicitly enable it.
import()).import { getDotenv } from '@karmaniverous/get-dotenv'import { createCli } from '@karmaniverous/get-dotenv/cli'import { GetDotenvCli, definePlugin } from '@karmaniverous/get-dotenv/cliHost'import { cmdPlugin, batchPlugin, awsPlugin, awsWhoamiPlugin, initPlugin } from '@karmaniverous/get-dotenv/plugins'import { resolveGetDotenvConfigSources } from '@karmaniverous/get-dotenv/config'import { overlayEnv } from '@karmaniverous/get-dotenv/env/overlay'dotenvToken: base dotenv filename token (default .env).privateToken: private suffix token (default local).env: selected environment string (e.g. dev, test).paths: ordered list of directories to search (later paths override earlier)..env*, private is .env.<privateToken>*.getdotenv.config.* and getdotenv.config.local.* layered on top of file-derived dotenv.For each path, up to four files are merged in this order (later wins):
<dotenvToken> (e.g. .env)<dotenvToken>.<env> (e.g. .env.dev)<dotenvToken>.<privateToken> (e.g. .env.local)<dotenvToken>.<env>.<privateToken> (e.g. .env.dev.local)Missing files are silently ignored.
Expansion happens recursively in strings:
$VAR[:default]${VAR[:default]}Unknown vars become empty string unless a default is provided. Escaped dollar signs (\$) remain literal.
Use helpers when you need the exact semantics:
import { dotenvExpand, dotenvExpandAll } from '@karmaniverous/get-dotenv';
Use the dotenv edit utilities when you need to update a .env* file in place without destroying comments, spacing, ordering, unknown lines, or line endings.
Pure text (no filesystem):
import { editDotenvText } from '@karmaniverous/get-dotenv';
const next = editDotenvText('A=1\n# keep\nB=2\n', {
A: 'updated',
UNUSED: null,
});
FS-level (deterministic target selection across paths, optional template bootstrap):
import { editDotenvFile } from '@karmaniverous/get-dotenv';
await editDotenvFile(
{ API_URL: 'https://example.com', UNUSED: null },
{
paths: ['./'],
scope: 'env',
privacy: 'private',
env: 'dev',
dotenvToken: '.env',
privateToken: 'local',
},
);
Notes:
null deletes key assignment lines (default).undefined skips (default); in mode: 'sync' an own key with undefined counts as present (so it is not deleted).JSON.stringify.duplicateKeys: 'all' | 'first' | 'last' (default: 'all').eol: 'preserve' | 'lf' | 'crlf' (default: 'preserve'), and trailing newline presence/absence is preserved.editDotenvFile) is deterministic and uses paths only (directories), matching get-dotenv’s precedence model by default:
searchOrder: 'reverse' (default): last path wins (highest precedence).searchOrder: 'forward': first path wins.<target>.<templateExtension> exists (default extension: template), the template is copied first and then edited in place.editDotenvFile returns { path, createdFromTemplate, changed } where path is absolute.Selector mapping (filename construction):
scope: 'global', privacy: 'public' → <dotenvToken>scope: 'env', privacy: 'public' → <dotenvToken>.<env>scope: 'global', privacy: 'private' → <dotenvToken>.<privateToken>scope: 'env', privacy: 'private' → <dotenvToken>.<env>.<privateToken>If scope: 'env' and neither env nor defaultEnv can be resolved, editDotenvFile throws with an env is required-style message.
Low-level building blocks (advanced use):
parseDotenvDocument(text) → parse into a format-preserving segment model.applyDotenvEdits(doc, updates, opts?) → apply merge/sync edits while preserving formatting.renderDotenvDocument(doc, eolMode?) → render back to text with EOL policy.getDotenv()Use getDotenv() when you want “compose an env map” without the CLI host/plugin system.
import { getDotenv } from '@karmaniverous/get-dotenv';
const env = await getDotenv({
env: 'dev',
paths: ['./'],
});
console.log(env.APP_SETTING);
getDotenv() options (what matters)All options are optional; important ones:
env?: string and/or defaultEnv?: string (used when env not provided)paths?: string[] (ordered)dotenvToken?: string (default .env)privateToken?: string (default local)vars?: ProcessEnv (explicit vars merged into the composed map)loadProcess?: boolean (default false for programmatic use; when true merges into process.env)dynamic?: Record<string, string | ((vars, env) => string | undefined)> (programmatic map; takes precedence over dynamicPath)dynamicPath?: string (path to JS/TS module default-exporting the same dynamic map)excludeDynamic?: boolean (skip dynamic evaluation)excludePublic|excludePrivate|excludeGlobal|excludeEnv?: booleanoutputPath?: string (writes a consolidated dotenv file; multiline values are quoted)log?: boolean and logger?: console-like (logs final map; can be combined with redaction/entropy options)redact?: boolean, redactPatterns?: Array<string | RegExp>warnEntropy?: boolean, entropyThreshold?: number, entropyMinLength?: number, entropyWhitelist?: Array<string | RegExp>Use defineDynamic() to get strong inference for your vars bag in TS:
import { defineDynamic, getDotenv } from '@karmaniverous/get-dotenv';
type Vars = { APP_SETTING?: string; ENV_SETTING?: string };
const dynamic = defineDynamic<Vars, { GREETING: (v: Vars) => string }>({
GREETING: ({ APP_SETTING = '' }) => `Hello ${APP_SETTING}`,
});
const env = await getDotenv<Vars>({ env: 'dev', paths: ['./'], dynamic });
Dynamic function signature:
(vars: ProcessEnv, env?: string) => string | undefinedReturn undefined to “unset/omit”.
When using the shipped CLI host (or embedding it via createCli/GetDotenvCli), config discovery + overlays are always active (and a no-op if no config exists).
getdotenv.config.json|yaml|yml|js|mjs|cjs|ts|mts|ctsgetdotenv.config.* (same extensions)getdotenv.config.local.* (same extensions)JSON/YAML configs are data-only. JS/TS configs may include dynamic + schema.
rootOptionDefaults?: { ... } (root CLI defaults; collapsed families; see below)rootOptionVisibility?: { [rootKey]: boolean } (help-time only; false hides options)scripts?: Record<string, string | { cmd: string; shell?: string | boolean }>vars?: Record<string, string> (global/public vars)envVars?: Record<string, Record<string, string>> (per-env/public vars)plugins?: Record<string, unknown> (per-plugin config slices keyed by realized mount path, e.g. aws/whoami)requiredKeys?: string[] (post-compose validation)dynamic?: Record<string, string | ((vars, env?) => string | undefined)>schema?: unknown (if it exposes safeParse(finalEnv), host runs it once post-compose)Do not put operational root flags (like shell, loadProcess, paths) at top level; they belong under rootOptionDefaults.
Higher wins:
rootOptionDefaultsrootOptionDefaultsrootOptionDefaultscreateCli({ rootOptionDefaults })baseRootOptionDefaultsVisibility precedence is similar (but no CLI flags for visibility):
rootOptionVisibilityrootOptionVisibilityrootOptionVisibilitycreateCli({ rootOptionVisibility })Config location:
plugins.<mountPath> where <mountPath> is the command path without the root alias, e.g.:
plugins.awsplugins['aws/whoami']Host behavior:
{ ...ctx.dotenv, ...process.env } (process.env wins for plugin slices).configSchema (if provided).plugin.readConfig(cli) (do not look up by id).The shipped CLI is plugin-first and includes: cmd, batch, aws (+ aws whoami), init.
Quick run:
npx @karmaniverous/get-dotenv -c 'echo $APP_SETTING'
Key root flags (behavioral intent):
-e, --env <string> select environment--paths <string> (delimited list) and delimiter options--dotenv-token <string>, --private-token <string>-s, --shell [string] (default OS shell when enabled), -S, --shell-off--capture or GETDOTENV_STDIO=pipe for deterministic CI output--trace [keys...] print child env diagnostics to stderr before spawning--redact / --redact-off, plus --redact-pattern <pattern...>--entropy-warn / --entropy-warn-off + threshold/min-length/whitelistImportant: the root “-c” behavior is owned by the cmd plugin (parent alias), not a root “--command” flag.
createCli()Use createCli to embed a ready-to-run CLI host and optionally customize composition.
#!/usr/bin/env node
import { createCli } from '@karmaniverous/get-dotenv/cli';
await createCli({ alias: 'toolbox' })();
Customize installed plugins:
import { createCli } from '@karmaniverous/get-dotenv/cli';
import { cmdPlugin, batchPlugin } from '@karmaniverous/get-dotenv/plugins';
const run = createCli({
alias: 'toolbox',
compose: (p) =>
p
.use(
cmdPlugin({ asDefault: true, optionAlias: '-c, --cmd <command...>' }),
)
.use(batchPlugin()),
});
await run();
GetDotenvCli and definePlugin()await program.resolveAndLoad(...).cli.getCtx() inside any plugin mount/action.readMergedOptions(thisCommand) to retrieve it.ctx.dotenvProvenance:
Record<string, DotenvProvenanceEntry[]> ordered in ascending precedence (last entry is effective).kind: 'file': path, scope (global/env), privacy (public/private).kind: 'config': scope, privacy, configScope (packaged/project).kind: 'vars': explicit CLI/programmatic overrides.kind: 'dynamic': dynamicSource (config | programmatic | dynamicPath).op: 'unset' is recorded when a layer returns undefined or explicitly unsets a key.groupPlugins(...)If you want a namespace-only parent command to group plugins under a shared prefix (for example, smoz getdotenv init), use groupPlugins rather than trying to “call” another plugin or inventing alias command names like getdotenv-init.
import { groupPlugins } from '@karmaniverous/get-dotenv/cliHost';
import { initPlugin } from '@karmaniverous/get-dotenv/plugins';
program.use(
groupPlugins({ ns: 'getdotenv', description: 'getdotenv tools' }).use(
initPlugin(),
),
);
Notes:
plugins['getdotenv/init']).cmdPlugin({ optionAlias: ... }) under the group, the alias attaches to the group command (e.g., smoz getdotenv -c ...), not the root.import { definePlugin } from '@karmaniverous/get-dotenv/cliHost';
export const helloPlugin = () =>
definePlugin({
ns: 'hello',
setup(cli) {
cli.description('Say hello').action(() => {
const ctx = cli.getCtx();
console.log('dotenv keys:', Object.keys(ctx.dotenv).length);
});
},
});
ns (command name).setup(cli) receives the mount and returns void..use(child, { ns: 'whoami2' }).If a plugin has config, attach configSchema and use instance-bound helpers:
plugin.readConfig(cli) to read the validated, interpolated slice.plugin.createPluginDynamicOption(cli, flags, (helpCfg, pluginCfg) => string) to render “effective default” help strings.import { buildSpawnEnv } from '@karmaniverous/get-dotenv';
const env = buildSpawnEnv(process.env, ctx.dotenv);
import {
readMergedOptions,
runCommand,
shouldCapture,
} from '@karmaniverous/get-dotenv/cliHost';
const bag = readMergedOptions(thisCommand);
const capture = shouldCapture(bag.capture);
await runCommand('echo OK', bag.shell ?? false, {
env,
stdio: capture ? 'pipe' : 'inherit',
});
Note: runCommand() re-emits buffered stdout when using stdio: 'pipe'; stderr is not re-emitted by default (use runCommandResult() if you need deterministic stderr handling).
Use shared helpers:
resolveCommand(scripts, input) resolves scripts[input] (string or { cmd }) or returns input.resolveShell(scripts, input, rootShell) uses scripts[input].shell if object form, else uses rootShell (or false when absent).node -e (avoid lossy tokenization)When running shell-off and passing a Node eval snippet, preserve argv:
import { maybePreserveNodeEvalArgv } from '@karmaniverous/get-dotenv/cliHost';
const argv = maybePreserveNodeEvalArgv(['node', '-e', 'console.log("ok")']);
Commander option parsers run before ctx exists, so they can only expand against process.env. If you want ${NAME} expansion based on the resolved dotenv context, expand at action-time against { ...process.env, ...ctx.dotenv }:
import { dotenvExpand } from '@karmaniverous/get-dotenv';
const raw = String(opts.tableName ?? '');
const envRef = { ...process.env, ...cli.getCtx().dotenv };
const expanded = dotenvExpand(raw, envRef) ?? raw;
This keeps behavior independent of loadProcess.
From the root export:
traceChildEnv({ parentEnv, dotenv, keys?, redact?, redactPatterns?, warnEntropy?, ... }) prints origin/value diagnostics for child env composition.redactDisplay(value, { redact?, redactPatterns? }) and redactObject(record, opts) mask values for logs/traces (presentation-only).maybeWarnEntropy(key, value, origin, opts, write) warns about likely secrets by entropy (presentation-only).cmd: executes a command under ctx; provides parent alias -c, --cmd <command...>; detects conflict when both alias and explicit subcommand are used.batch: discovers directories by globs and runs a command sequentially; honors --list and --ignore-errors.aws: establishes a session once per invocation and writes AWS env vars to process.env; publishes minimal metadata under ctx.plugins.aws; supports strategy: none to disable credential export.init: scaffolds config files and a CLI skeleton under src/cli/<name>/...; collision handling supports overwrite/example/skip plus CI heuristics.This section exists to answer common “can I depend on this?” questions when authoring third-party plugins intended to interoperate with the shipped plugins.
parentPlugin().use(childPlugin())definePlugin() can be nested under another plugin via .use(childPlugin()); the shipped plugins follow this model.awsPlugin() is explicitly designed to act as a parent for AWS-dependent child plugins. Prefer mounting your plugin under aws so session/region/credential resolution happens before your code runs.Canonical wiring example (child plugin mounted under aws):
#!/usr/bin/env node
import { createCli } from '@karmaniverous/get-dotenv/cli';
import { awsPlugin } from '@karmaniverous/get-dotenv/plugins';
import { secretsPlugin } from '@acme/aws-secrets-plugin'; // example third-party plugin
await createCli({
alias: 'toolbox',
compose: (program) => program.use(awsPlugin().use(secretsPlugin())),
})();
Notes:
aws secrets, the config key is plugins['aws/secrets'].cmd plugin’s parent alias (-c, --cmd <command...>) is attached to the command it is mounted under. If you mount cmdPlugin() under a group/namespace command, the alias attaches to that group (not the root).ctx.plugins.* shapes (what is safe to depend on)aws plugin currently publishes a stable, documented entry under ctx.plugins: ctx.plugins.aws.ctx.plugins.aws contains non-sensitive metadata only. Treat this as the stable surface:
profile?: stringregion?: stringctx.plugins. If your child plugin needs credentials, rely on the standard AWS SDK v3 provider chain reading from process.env after the aws parent runs.Other shipped plugins (cmd, batch, init) do not currently publish stable ctx.plugins.* entries. If you observe additional fields in ctx.plugins, treat them as internal/unstable unless they are documented as part of a stable contract.
.env*)If your plugin edits dotenv files (e.g., syncing secrets into .env.<env>.<privateToken>), prefer selecting and editing a single target using editDotenvFile(...) rather than writing to every path:
editDotenvFile deterministically selects the first match across paths and edits only that file (or bootstraps from a sibling template when needed).searchOrder: 'reverse' (last path wins).Some X-Ray SDK integrations throw if AWS_XRAY_DAEMON_ADDRESS is not set. Do not import or enable X-Ray capture unconditionally:
AWS_XRAY_DAEMON_ADDRESS is present, or when an explicit “xray on” option is enabled and you validate required env up front.Use this section when you need a “what do I import?” answer quickly.
@karmaniverous/get-dotenv):
getDotenv, defineDynamic, defineGetDotenvConfigbaseRootOptionDefaultsdotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnveditDotenvText, editDotenvFile (format-preserving)traceChildEnv, redactDisplay, redactObject, maybeWarnEntropyinterpolateDeep (deep string-leaf interpolation), writeDotenvFile, defaultsDeep, tokenize@karmaniverous/get-dotenv/cli):
createCli({ alias, branding?, compose?, rootOptionDefaults?, rootOptionVisibility? }) -> (argv?) => Promise<void>@karmaniverous/get-dotenv/cliHost):
GetDotenvClidefinePlugin (returns a plugin with readConfig and createPluginDynamicOption)runCommand, runCommandResult, shouldCapture, buildSpawnEnvreadMergedOptionsresolveCommand, resolveShell, defineScriptsgetRootCommand, composeNestedEnv, maybePreserveNodeEvalArgv@karmaniverous/get-dotenv/plugins):
cmdPlugin, batchPlugin, awsPlugin, awsWhoamiPlugin, initPlugin@karmaniverous/get-dotenv/config):
resolveGetDotenvConfigSources(...) and validation helpers used by the host@karmaniverous/get-dotenv/env/overlay):
overlayEnv({ base, env, configs, programmaticVars? })process.env contains the resolved dotenv context; use cli.getCtx().dotenv unless you explicitly enabled loadProcess.ctx.dotenv; expand at action time.plugin.readConfig(cli) and the instance-bound dynamic option helper.