Plugins are small modules that register commands and behavior against the plugin‑first host. The host resolves dotenv context once per invocation, overlays config, validates, and then runs your plugin’s setup and optional afterResolve hooks. See also: Config & Validation, Diagnostics & Errors, and Executing Shell Commands.
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('hello', Object.keys(ctx.dotenv).length);
});
},
});
#!/usr/bin/env node
import { createCli } from '@karmaniverous/get-dotenv/cli';
import {
cmdPlugin,
batchPlugin,
awsPlugin,
initPlugin,
} from '@karmaniverous/get-dotenv/plugins';
import { helloPlugin } from './plugins/hello';
const run = createCli({
alias: 'toolbox',
branding: 'Toolbox CLI',
compose: (p) =>
p
.use(
cmdPlugin({ asDefault: true, optionAlias: '-c, --cmd <command...>' }),
)
.use(batchPlugin())
.use(awsPlugin())
.use(initPlugin())
.use(helloPlugin()),
});
await run(); // or await run(process.argv.slice(2));
When you need the real root command (for branding, alias labels, or reading root‑level metadata) from inside a plugin mount or action, use the small typed helper exported by the host:
import {
definePlugin,
getRootCommand,
} from '@karmaniverous/get-dotenv/cliHost';
export const helloPlugin = () => {
const plugin = definePlugin({
ns: 'hello',
setup(cli) {
cli.action(() => {
const root = getRootCommand(cli); // typed Commander root
// Example: include the root name in your output
console.log(`[${root.name()}] hello from plugin`);
});
},
});
return plugin;
};
For a fuller example in context, see the scaffolded template plugin at templates/cli/plugins/hello/index.ts.
For non‑trivial plugins, prefer the structure used by the shipped template (init scaffold) to keep setup clean and wiring testable:
index.ts — The plugin factory. Calls definePlugin and orchestrates setup by calling attach helpers.options.ts — Exports attach*Options(cli). Attaches options/arguments and returns the command for chaining.*Action.ts — Exports attach*Action(cli). Wires the .action() handler.types.ts — Exports Zod configuration schemas and inferred types.This separation (“bucket of subcommands”) scales well as your plugin grows.
Notes:
compose hook runs before parsing and is the recommended way to assemble your CLI surface. The factory installs root options and hooks so shipped plugins can read the merged options bag with readMergedOptions().createCli is simpler and safer.When using the factory, prefer the branding option:
await createCli({
alias: 'toolbox',
branding: 'toolbox v1.0.0', // optional help header
compose: (p) => p /* ...wire plugins... */,
})();
If you construct a host directly, call program.brand({ importMetaUrl, description }) before parsing.
Inside actions, prefer the structural helpers:
import { readMergedOptions } from '@karmaniverous/get-dotenv/cliHost';
cli.ns('print').action((_args, _opts, thisCommand) => {
const bag = readMergedOptions(thisCommand);
const ctx = cli.getCtx();
// bag contains merged root options (scripts, shell, capture, trace, etc.)
// ctx.dotenv contains the final merged env (after overlays and dynamics)
});
If your plugin accepts option values that may include environment references (for example, --table-name '${TABLE_NAME}'), expand them yourself at action time using the resolved context (so you are not dependent on loadProcess). See Executing Shell Commands for recommended patterns and cross-platform quoting guidance.
See also:
Prefer the plugin‑bound helper createPluginDynamicOption to render help text that reflects the resolved configuration for that specific plugin instance. The host evaluates dynamic descriptions with overlays and dynamic enabled, without logging or mutating the environment.
Key points:
Example (plugin flag with ON/OFF default label using instance‑bound config):
import { z } from 'zod';
import { definePlugin } from '@karmaniverous/get-dotenv/cliHost';
const helloConfigSchema = z.object({
loud: z.boolean().optional().default(false),
});
type HelloConfig = z.infer<typeof helloConfigSchema>;
export const helloPlugin = () => {
const plugin = definePlugin({
ns: 'hello',
configSchema: helloConfigSchema,
setup(cli) {
cli
.description('Say hello with current dotenv context')
.addOption(
// Helper infers config type from plugin instance
plugin.createPluginDynamicOption(
cli,
'--loud',
(
_bag,
cfg, // cfg is inferred as Readonly<HelloConfig>
) => `print greeting in ALL CAPS${cfg.loud ? ' (default)' : ''}`,
),
)
.action(() => {
const cfg = plugin.readConfig(cli); // cfg is inferred
// use cfg.loud at runtime
});
},
});
return plugin;
};
Or build the Option first:
const opt = plugin.createPluginDynamicOption(
cli,
'--loud',
(_bag, cfg) => `print greeting in ALL CAPS${cfg.loud ? ' (default)' : ''}`,
);
cli.addOption(opt);
Notes:
Update (DX: no undefined):