0Z4C0
// navigation
// @ozaco/std

Config

Hierarchical config discovery, merge and edit across a directory chain.

import from@ozaco/std/config

Config is a plugin that discovers <name>.<ext> files from cwd up to home, resolves each file's extends, and merges the whole chain — plus the active-variant overlay, the config-directory files, and an environment overlay — into one view.

Precedence within a level is variant → dir → base; inner directory levels win over outer ones; and the ENV overlay wins over everything. Which layers run is controlled by Features bitflags (all enabled by default). It requires an IO implementation and a codec (TOML by default) installed first.

Config.actions.* operate on the scope-installed singleton config, covering the full lifecycle: load/refresh/save, read with get/has/keys/search, edit with set/remove/clear/delete, inspect provenance with tree/origin/explain, and react to changes with watch. Config.actions.open(options) mints extra, fully independent instances for managing several configs at once.

// on this page
01

Plugin

The config plugin and its actions.

const

Config

The config plugin — discovers, merges and edits a hierarchy of config files.

signature
const Config: Plugin<
  ConfigDef.Context,
  [options?: ConfigDef.Options],
  ConfigDef.Actions
>

// Config.actions operate on the scope-installed (singleton) config:
Config.actions.load(cwd?: string): Future<void>       // (re)discover + merge
Config.actions.refresh(): Future<void>                // re-run discovery in place
Config.actions.save(path?: string): Future<void>      // flush edited files
Config.actions.get<T>(key?: string): Future<T>        // read dotted key (or all)
Config.actions.set(key: string, value: unknown): Future<void>
Config.actions.remove(key: string): Future<void>
Config.actions.clear(): Future<void>                  // empty the working file
Config.actions.delete(path?: string): Future<void>    // delete a file, re-discover
Config.actions.search(query: string): Future<ConfigDef.Match[]>
Config.actions.tree(): Future<ConfigDef.Source[]>
Config.actions.has(key: string): Future<boolean>
Config.actions.keys(): Future<string[]>
Config.actions.origin(key: string): Future<string | undefined>
Config.actions.explain(key: string): Future<ConfigDef.Origin[]>
Config.actions.watch(
  listener: ConfigDef.Watcher,
  options?: ConfigDef.WatchOptions,
): Future<Task<void>>
Config.actions.open(options?: ConfigDef.Options): Future<ConfigDef.Instance>

The plugin is a scope singleton, so a second install overwrites the first — use open to run several independent configs side by side. Edits from set/remove/clear are applied to the merged view immediately and in memory; call save to persist them to disk.

example
import { install } from '@ozaco/std/plugin'
import { Config } from '@ozaco/std/config'

yield* install(Config, { name: 'ozaco' })
yield* Config.actions.load()

const port = yield* Config.actions.get<number>('server.port')
yield* Config.actions.set('server.port', 8080)
yield* Config.actions.save()

// Trace where a key comes from.
const where = yield* Config.actions.explain('server.port')

// An independent second config with its own discovery + working file.
const other = yield* Config.actions.open({ name: 'tsconfig', ext: 'json' })
const strict = yield* other.get<boolean>('compilerOptions.strict')
02

Types

type

ConfigDef

The config plugin shape and its associated value types.

signature
type ConfigDef = Plugin<ConfigDef.Context, [options?: ConfigDef.Options], ConfigDef.Actions>

The ConfigDef namespace also exposes:

  • ObjectRecord<string, unknown>, a codec-parsed config value.
  • Match{ key: string; value: unknown }, a search hit.
  • Origin{ path: string; value: unknown }, one provenance entry ('<env>' = overlay).
  • Source — a discovered file: its path, data, resolved extends, and raw extendsSpec.
  • Watcher(merged: ConfigDef.Object) => void, the watch change listener.
  • WatchOptions{ debounce?: number } for coalescing filesystem events.
  • Context — the internal merged state (chain, env overlay, merged view, working file, dirty set).
  • Instance / Actions — the method surface (see below).
iface

ConfigDef.Options

Discovery and merge options passed on install or to `open`.

signature
interface Options {
  codec?: CodecDef        // target codec (default TomlCodec)
  name?: string           // config name (default 'ozaco')
  cwd?: string            // discovery start dir (default process.cwd())
  variant?: string        // active variant overlay (default STD_CONFIG env)
  home?: string           // discovery stop dir, inclusive (default home)
  dot?: boolean           // dir/file should start with a dot (default true)
  ext?: string            // file extension w/o dot (default derived from codec)
  features?: Features     // enabled features (default Features.ALL)
}
iface

ConfigDef.Instance

A self-contained config with its own discovery, merge and working file.

signature
interface Instance {
  load(cwd?: string): Future<void>
  refresh(): Future<void>
  save(path?: string): Future<void>
  get<T>(key?: string): Future<T>
  set(key: string, value: unknown): Future<void>
  remove(key: string): Future<void>
  clear(): Future<void>
  delete(path?: string): Future<void>
  search(query: string): Future<ConfigDef.Match[]>
  tree(): Future<ConfigDef.Source[]>
  has(key: string): Future<boolean>
  keys(): Future<string[]>
  origin(key: string): Future<string | undefined>
  explain(key: string): Future<ConfigDef.Origin[]>
  watch(
    listener: ConfigDef.Watcher,
    options?: ConfigDef.WatchOptions,
  ): Future<Task<void>>
}

Config's own actions are the default instance; ConfigDef.Actions extends this with open, which returns a fresh, independent Instance. watch returns the background Taskhalt() it to stop watching.

03

Constants

Feature flags and discovery defaults.

const

Features

Bitflags toggling each config discovery/merge layer. Combine with `|`.

signature
enum Features {
  NONE = 0,
  FILE = 1 << 2,    // read the base <name>.<ext> at each level
  CHAIN = 1 << 3,   // walk parent dirs cwd -> home, merging outer -> inner
  VARIANT = 1 << 4, // apply the <variant>.<name>.<ext> overlay per level
  ENV = 1 << 5,     // read STD_CONFIG + overlay <NAME>_A_B env vars on top
  DIR = 1 << 6,     // also merge a <name>/ directory's *.<ext> files
  ALL = FILE | CHAIN | VARIANT | ENV | DIR,
}

Each flag toggles one discovery layer independently; Features.ALL (the default) enables every layer.

example
import { Features } from '@ozaco/std/config'
import { Config } from '@ozaco/std/config'
import { install } from '@ozaco/std/plugin'

// Single file only, no chain walking or env overlay.
yield* install(Config, { features: Features.FILE })
const

DEFAULT_NAME

Default config name (basename of the discovered files).

signature
const DEFAULT_NAME = 'ozaco'
const

EXTENDS_KEY

Key inside a config file listing other files to inherit from.

signature
const EXTENDS_KEY = 'extends'

Extended paths are resolved relative to the file that declares them.

const

VARIANT_ENV_KEY

Env var naming the active variant when the `ENV` feature is on.

signature
const VARIANT_ENV_KEY = 'STD_CONFIG'