This section documents the plugins shipped with the get‑dotenv CLI:
aws subcommand for session and forwarding.--cmd alias.#!/usr/bin/env node
import type { Command } from 'commander';
import { GetDotenvCli } from '@karmaniverous/get-dotenv/cliHost';
import { batchPlugin } from '@karmaniverous/get-dotenv/plugins';
const program: Command = new GetDotenvCli('mycli').use(batchPlugin());
await (program as unknown as GetDotenvCli).resolveAndLoad();
await program.parseAsync();
resolveAndLoad() produces a context { optionsResolved, dotenv, plugins? }.This library ships a small set of ready‑to‑use plugins. Most projects want the same baseline:
-e/--env, --paths, --strict, --trace, etc.)-c, --cmd <command...>) for one‑off commandsHere is a minimal host that wires all of the above.
#!/usr/bin/env node
import { GetDotenvCli } from '@karmaniverous/get-dotenv/cliHost';
import {
cmdPlugin,
batchPlugin,
awsPlugin,
initPlugin,
} from '@karmaniverous/get-dotenv/plugins';
const program = new GetDotenvCli('toolbox');
// Optional: nice help header (uses your package.json version when importMetaUrl is provided)
await program.brand({
importMetaUrl: import.meta.url,
description: 'Toolbox CLI',
});
// 1) Attach base root options (-e/--env, --paths, --strict, --trace, etc.)
// 2) Install included plugins
// 3) passOptions() merges root flags (parent < current), computes the dotenv context
// once per invocation, runs validation, and persists merged options for nested flows.
program
.attachRootOptions({ loadProcess: false })
.use(cmdPlugin({ asDefault: true, optionAlias: '-c, --cmd <command...>' }))
.use(batchPlugin())
.use(awsPlugin())
.use(initPlugin())
.passOptions({ loadProcess: false });
await program.parseAsync();
Helpers availability
program.attachRootOptions(...)program.passOptions(...)@karmaniverous/get-dotenv/cliHost.Note: Per‑plugin subpaths (e.g., @karmaniverous/get-dotenv/plugins/cmd) remain available, but the plugins barrel is recommended to share a single type identity with cliHost and simplify imports.
Put defaults under getdotenv.config.*. The loader overlays packaged → project/public → project/local and then applies dynamic (when present). Plugin slices live under the plugins key.
JSON example:
{
"paths": "./",
"dotenvToken": ".env",
"privateToken": "local",
"plugins": {
"batch": {
"scripts": {
"build": { "cmd": "npm run build", "shell": "/bin/bash" },
"plain": { "cmd": "node -v", "shell": false }
},
"shell": true,
"rootPath": "./packages",
"globs": "*",
"pkgCwd": false
},
"aws": {
"profile": "dev",
"defaultRegion": "us-east-1",
"loginOnDemand": true,
"setEnv": true,
"addCtx": true
}
}
}
TypeScript example (JS/TS allows dynamic too):
export default {
plugins: {
batch: {
scripts: { build: { cmd: 'npm run build', shell: '/bin/bash' } },
shell: false,
},
aws: {
profileKey: 'AWS_LOCAL_PROFILE',
regionKey: 'AWS_REGION',
strategy: 'cli-export',
loginOnDemand: true,
},
},
};
toolbox -e dev -c 'node -e "console.log(process.env.APP_SETTING ?? \"\")"'
toolbox batch -r ./services -g "web api" -l
# session only (populate process.env and ctx.plugins.aws)
toolbox aws --profile dev --login-on-demand
# forward to AWS CLI (tokens after -- are passed through)
toolbox aws -- sts get-caller-identity
Use the branding helper to set the CLI’s name/description and automatically display the version in help when branding is active. When you provide importMetaUrl, the host resolves the nearest package.json and uses its version. If you don’t pass a helpHeader, the host prints a sensible default <name> v<version> as a header.
#!/usr/bin/env node
import type { Command } from 'commander';
import { GetDotenvCli } from '@karmaniverous/get-dotenv/cliHost';
const program: GetDotenvCli = new GetDotenvCli('toolbox');
await program.brand({
importMetaUrl: import.meta.url, // resolves version from your package.json
description: 'Toolbox CLI', // optional
// helpHeader: 'My Toolbox v1.2.3', // optional; default uses "<name> v<version>"
});
// … wire options/plugins …
await program.parseAsync();
Notes and behavior details:
createCli(...).run(argv). To keep -h/--help deterministic and avoid process.exit, the binary prints help and returns before branding runs. As a result, the header may be omitted for top‑level -h/--help.getdotenv help (which flows through normal parsing and branding), orbrand() before parseAsync().You can add your own root options and group them under “App options” in help using tagAppOptions. Install base flags and merging with attachRootOptions().passOptions() so values flow into the merged options bag:
import type { Command } from 'commander';
import {
GetDotenvCli,
readMergedOptions,
} from '@karmaniverous/get-dotenv/cliHost';
import { definePlugin } from '@karmaniverous/get-dotenv/cliHost';
const program: GetDotenvCli = new GetDotenvCli('mycli');
await program.brand({ importMetaUrl: import.meta.url, description: 'My CLI' });
program
.attachRootOptions() // install base flags (-e, --paths, etc.)
.tagAppOptions((root) => {
root.option('--foo <value>', 'custom app option'); // appears under "App options" in -h
})
.use(
definePlugin({
id: 'print',
setup(cli) {
cli.ns('print').action((_args, _opts, thisCommand) => {
// Read the merged root options bag. Custom keys flow through.
const bag = readMergedOptions(thisCommand) ?? {};
const foo = (bag as unknown as { foo?: string }).foo;
console.log(`foo=${foo ?? ''}`);
});
},
}),
)
.passOptions(); // merge flags (parent < current), compute dotenv context
await program.parseAsync();
Example:
mycli -e dev --foo bar print
# -> foo=bar
Notes:
tagAppOptions only affects help grouping; values are parsed by Commander and merged by passOptions().readMergedOptions(thisCommand) in actions (or cli.getOptions() from code that has a handle on the host) to read the merged options bag. Custom keys are present at runtime even if not in the default type.import type { GetDotenvCli } from '@karmaniverous/get-dotenv/cliHost';
import { definePlugin } from '@karmaniverous/get-dotenv/cliHost';
export const myPlugin = definePlugin({
id: 'my',
setup(cli: GetDotenvCli) {
cli
.ns('my')
.description('My commands')
.action(async () => {
const ctx = cli.getCtx?.();
// Use ctx.dotenv or ctx.plugins['my']...
});
},
async afterResolve(cli, ctx) {
// Initialize any clients/secrets using ctx.dotenv
// Attach per-plugin state under ctx.plugins by convention:
ctx.plugins ??= {};
ctx.plugins['my'] = { ready: true };
},
});
Composition:
const parent = definePlugin({ id: 'parent', setup() {} })
.use(definePlugin({ id: 'childA', setup() {} }))
.use(definePlugin({ id: 'childB', setup() {} }));
Plugins install parent → children; afterResolve also runs parent → children.
Prefer passing the resolved env explicitly to subprocesses invoked by your commands:
import { execaCommand } from 'execa';
// inside a command action:
const ctx = cli.getCtx?.();
await execaCommand('node -e "console.log(process.env.MY_VAR)"', {
env: { ...process.env, ...ctx.dotenv },
stdio: 'inherit',
});
The shipped CLI uses a nested-CLI strategy by placing the merged CLI options on process.env.getDotenvCliOptions (JSON) so child getdotenv invocations can inherit the parent’s defaults and flags.
Use the built-in scaffolder to create config files and a starter CLI:
# JSON config + .local + CLI skeleton named “acme”
getdotenv init . --config-format json --with-local --cli-name acme --force
Notes:
--yes unless --force.__CLI_NAME__ tokens with your chosen name.--yes (Skip All) when stdin or stdout is not a TTY OR when a CI-like environment variable is present (CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD).--force > --yes > auto-detect (non-interactive => Skip All).The plugin host and the generator use the config loader/overlay path by default (always-on). When no config files are present, the loader is a no-op. Validation runs once after Phase C (interpolation) and respects --strict; redaction/entropy diagnostics apply to --trace and -l/--log without altering runtime values. See the “Config files and overlays” guide (./config.md) for discovery, precedence, validation, and diagnostics.