Skip to content

Configuration ​

The standard library provides a simple but powerful configuration system built on top of:

  • A --config / -c option
  • A configPlugin that wires the option into your CLI and handles the config file loading, merging and validation
  • A set of services and utils to resolve and merge config files if you need more control

This page documents how to use it in real CLIs.

Config Plugin Behavior ​

The configPlugin provides a simplified configuration system that:

  • Delegates config loading and merging to c12.loadConfig (via c12Options)
  • Supports an explicit config file via --config / -c
  • Validates the final resolved configuration against a Zod schema if schema is provided (via validateConfig(), throwing ConfigValidationError on failure)

Configuration Sources (c12) ​

The plugin delegates config resolution to c12.loadConfig.

  • For the default behavior, c12Options are forwarded directly to c12 (this includes things like dotenv, rc files, environment-specific overrides, extends, etc.).
  • If --config / -c is provided, that file is used as the main config for this run (with rc/packageJson disabled for the explicit file).

Advanced: pass c12 options ​

If you need more control over how config is resolved (for example: dotenv, rc files, env-specific keys, remote layers via extends, etc.), pass them directly via c12Options.

ts
import z from "zod";

configPlugin({
  schema: z.object({ outputDir: z.string() }),
  c12Options: {
    dotenv: true,
    envName: "development",
  },
});

Using the std config option and plugin ​

Adding the config option manually ​

You can register the std config option directly via bequeathOptions on the root command:

ts
import { createCli, defineRootCommand } from "cheloni";
import { configOption } from "cheloni/std";

const rootCommand = defineRootCommand({
  bequeathOptions: [configOption],
  commands: [/* ... */],
});

const cli = await createCli({
  name: "my-cli",
  command: rootCommand,
});

Adding via configPlugin ​

Most CLIs should use the plugin instead of wiring the option manually:

ts
import { createCli } from "cheloni";
import { configPlugin } from "cheloni/std";

const cli = await createCli({
  name: "my-cli",
  version: "1.0.0",
  command: rootCommand,
  plugins: [configPlugin()],
});

configPlugin:

  • Registers the config option on the CLI
  • Ensures --config / -c is available to all commands

Plugin Options ​

  • c12Options?: LoadConfigOptions — forwarded to c12.loadConfig
  • schema?: z.ZodTypeAny — validates the final resolved config and throws if invalid

What the plugin handler does ​

The std configPlugin handler:

  • Delegates config resolution/merging to c12.loadConfig using c12Options (and uses --config as the main config file when provided)
  • Validates against schema (if provided) using validateConfig() and throws a ConfigValidationError on failure (the original Zod error is kept as the cause)
  • Exposes the validated result on the execution context via configMiddleware:
    • ctx.config – merged configuration object (always defined, at least {})
    • ctx.configFile – the resolved main config file (when available)

Example handler usage:

ts
const build = defineCommand({
  name: "build",
  handler: async ({ ctx }) => {
    const cfg = ctx.config as any;
    const outputDir = cfg?.outputDir ?? "dist";

    console.log(`Building into: ${outputDir}`);
  },
});

Note
If you provide a schema option, the config will be validated and typed.
Otherwise, treat ctx.config as unknown and narrow/validate it in user land as needed.

Using c12 directly ​

For custom configuration flows, cheloni/std re-exports loadConfig from c12:

ts
import { loadConfig } from "cheloni/std";

const { config } = await loadConfig({
  name: "my-cli",
  // cwd, configFile, dotenv, overrides, extends, ...
});

If you also want to validate a resolved config yourself, cheloni/std exports validateConfig():

ts
import { validateConfig } from "cheloni/std";
import z from "zod";

const schema = z.object({ outputDir: z.string() });
const validatedConfig = validateConfig(config, schema);