@happyvertical/smrt-config

Configuration management with cosmiconfig, secret sanitization, and SSG-safe export. Loads smrt.config.{js,ts,json} with globalThis caching so every package sees one config instance.

v0.29.34Core FoundationESM

Overview

@happyvertical/smrt-config is the configuration backbone for the SMRT framework. It uses cosmiconfig to load smrt.config.{js,ts,json} from the project root, deep-merges runtime overrides on top, and caches the result on globalThis so every package -- and every module instance -- sees the same merged config.

How It Works

  1. loadConfig() uses cosmiconfig to find smrt.config.{js,ts,json}
  2. Merge priority (highest → lowest): runtime overrides (setConfig()) > packages/modules section > global smrt section > caller defaults
  3. The merged result is cached on globalThis.__smrtConfigCache -- every getConfig() / getPackageConfig() / getModuleConfig() call returns the same object

Installation

bash
npm install @happyvertical/smrt-config

Quick Start

1. Create Configuration File

Create smrt.config.js (or .ts / .json) in your project root:

javascript
// smrt.config.js
export default {
  smrt: {
    cacheDir: '.cache',
    logLevel: 'info',
  },

  packages: {
    ai: {
      defaultProvider: 'anthropic',
      defaultModel: 'claude-sonnet-4-20250514',
      apiKeys: {
        anthropic: process.env.ANTHROPIC_API_KEY,
      },
    },
  },

  modules: {
    'town-scraper': {
      cronSchedule: '0 0 * * *',
      maxPages: 100,
    },
  },
};

2. Load Configuration

typescript
import { loadConfig } from '@happyvertical/smrt-config';

// Call once at app startup -- result is cached on globalThis
await loadConfig();

3. Use Configuration

typescript
import { getPackageConfig, getModuleConfig, setConfig } from '@happyvertical/smrt-config';

// Package-scoped (used by every @happyvertical/smrt-* package)
const aiConfig = getPackageConfig('ai', {
  defaultProvider: 'openai',
  defaultModel: 'gpt-4',
});

// Module-scoped (per-app modules)
const scraperConfig = getModuleConfig('town-scraper', {
  cronSchedule: '0 0 * * *',
  maxPages: 50,
});

// Runtime override -- highest priority
setConfig({
  packages: {
    ai: { defaultModel: 'gpt-4-turbo' },
  },
});

API Reference

FunctionPurpose
loadConfig(options?)Async load from file via cosmiconfig
getConfig()Get the full merged config
getPackageConfig(name, defaults?)Package-scoped config section (used by all @happyvertical/smrt-* packages)
getModuleConfig(name, defaults?)Module-scoped config section (per-app modules)
setConfig(overrides)Runtime overrides (highest priority)
clearCache()Reset cached config -- affects all modules
defineConfig(config)Type-safe config-file helper
exportConfig(options?)SSG-safe export (defaults to no secrets)
sanitizeConfig(config)Strip keys matching: apiKey, password, secret, token, credential, private, auth, key

getPackageConfig<T>()

This is the canonical pattern that @happyvertical/smrt-* packages use to read their own config section (you'll see it in smrt-prompts, smrt-languages, smrt-features, smrt-core, and many more):

typescript
import { getPackageConfig } from '@happyvertical/smrt-config';

interface AIConfig {
  defaultProvider: string;
  defaultModel: string;
  temperature: number;
}

const aiConfig = getPackageConfig<AIConfig>('ai', {
  defaultProvider: 'openai',
  defaultModel: 'gpt-4',
  temperature: 0.7,
});

defineConfig() for type-safe config files

typescript
// smrt.config.ts
import { defineConfig } from '@happyvertical/smrt-config';

export default defineConfig({
  smrt: { logLevel: 'info' },
  packages: {
    ai: { defaultProvider: 'anthropic' },
  },
});

SSG-Safe Export

exportConfig() is designed for static site generation -- it strips secrets by default so you can safely inline config into bundled output:

typescript
import { exportConfig, sanitizeConfig } from '@happyvertical/smrt-config';

// Default: secrets stripped
const safe = exportConfig();

// Opt-in to keep secrets (rare -- typically only for internal CLIs)
const full = exportConfig({ includeSecrets: true });

// Manual sanitization (strips keys matching:
//   apiKey, password, secret, token, credential, private, auth, key)
const sanitized = sanitizeConfig(config);

Testing

Use setConfig() to inject test-specific values and clearCache() in afterEach to reset state between tests:

typescript
import { beforeEach, afterEach, test, expect } from 'vitest';
import { setConfig, clearCache, getPackageConfig } from '@happyvertical/smrt-config';

beforeEach(() => {
  setConfig({
    packages: {
      ai: { defaultProvider: 'mock', defaultModel: 'test-model' },
    },
  });
});

afterEach(() => {
  clearCache(); // reset shared state
});

test('uses test configuration', () => {
  const config = getPackageConfig('ai');
  expect(config.defaultProvider).toBe('mock');
});

Key Files

  • src/loader.ts -- cosmiconfig integration and file discovery
  • src/merge.ts -- deep-merge logic and runtime config store
  • src/export.ts -- sanitization and JSON/JS export formatting
  • src/types.ts -- full config schema (~800 lines)

Gotchas

  • clearCache() is global: it affects every package and module reading from globalThis.__smrtConfigCache. Call it carefully outside of tests.
  • SSG export defaults to no secrets: you must explicitly pass { includeSecrets: true } to keep them.
  • Deep merge: later values override earlier ones at each key level (objects merge, scalars/arrays replace).
  • Reference via env vars: keep secrets in process.env and reference them from smrt.config.js. Don't hardcode secrets in the config file.

Used By

Every @happyvertical/smrt-* package that needs configuration reads its section via getPackageConfig(). Recent consumers include:

Next Steps