@push.rocks/smartconfig 🚀

A comprehensive TypeScript configuration management library providing centralized tool configs, persistent key-value storage, and powerful environment variable mapping with automatic type conversions.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install 📦

npm install @push.rocks/smartconfig --save
# or
pnpm add @push.rocks/smartconfig

Quick Start ⚡

import { Smartconfig, AppData, KeyValueStore } from '@push.rocks/smartconfig';

// 1. Read tool config from .smartconfig.json
const sc = new Smartconfig();
const eslintOpts = sc.dataFor('eslint', { extends: 'standard' });

// 2. Map env vars to typed config (with auto-conversion)
const appData = await AppData.createAndInit<{ port: number; debug: boolean }>({
  envMapping: {
    port: 'PORT',
    debug: 'boolean:DEBUG',
  },
});

// 3. Persist data between runs
const kv = new KeyValueStore({ typeArg: 'userHomeDir', identityArg: 'myapp' });
await kv.writeKey('lastRun', Date.now());

Three Core Classes 🏗️

1. Smartconfig — Centralized Tool Configuration

Reads a .smartconfig.json file from a project directory and merges its contents with your defaults. One file, every tool.

import { Smartconfig } from '@push.rocks/smartconfig';

const sc = new Smartconfig();              // uses cwd
const sc2 = new Smartconfig('/my/project'); // or specify a path

const prettierConfig = sc.dataFor<PrettierConfig>('prettier', {
  semi: false,
  singleQuote: true,
});

.smartconfig.json example:

{
  "prettier": {
    "semi": true,
    "printWidth": 120
  },
  "eslint": {
    "extends": "@company/eslint-config"
  }
}

Values from the file override the defaults you pass in. Missing keys fall back to your defaults.

Properties:

  • smartconfigJsonExists: boolean — whether .smartconfig.json was found
  • smartconfigJsonData: any — the parsed JSON contents

Methods:

  • dataFor<T>(toolName: string, defaults: T): T — returns merged config
  • writeDataFor<T>(toolName: string, data: T, options?: { createIfMissing?: boolean }): Promise<void> — replaces one tool's block in .smartconfig.json

Writing a tool block ✍️

A tool can update its own block without touching anyone else's:

import { Smartconfig, SmartconfigChangedError } from '@push.rocks/smartconfig';

const sc = new Smartconfig('/my/project');
const current = sc.dataFor<{ allowlist: string[] }>('mytool', { allowlist: [] });

try {
  await sc.writeDataFor('mytool', { ...current, allowlist: [...current.allowlist, 'new-entry'] });
} catch (error) {
  if (error instanceof SmartconfigChangedError) {
    // someone else changed .smartconfig.json since `sc` read it: nothing was written,
    // create a new Smartconfig to read the current file and decide again
  }
  throw error;
}
  • One key, nothing else. Only the top-level key toolName is replaced. Every other key keeps its value and its position; a tool without a block gets one appended at the end. The file is written as JSON.stringify(document, null, 2) plus a trailing newline, the format @git.zone/cli writes, so a file already in that format changes only in the replaced block. A file in another format is rewritten in this one.
  • No lost updates. The instance keeps the exact file content it read in its constructor. A write is refused with SmartconfigChangedError (carrying path) when the file holds anything else by then, including when it was removed or created elsewhere. After a successful write the instance's snapshot and smartconfigJsonData hold the written document, so the same instance can write again; concurrent writes through one instance run one after the other.
  • Serialized across processes. The check and the write run under the lock file .smartconfig.json.lock next to the config. A writer waits up to 5 seconds for a held lock, then fails with the holder's pid and creation time. A lock left behind by a process that died is never broken automatically: once that process is gone, remove the lock file by hand.
  • Atomic. The new content goes to a temporary file in the same directory that is renamed over .smartconfig.json, so readers see either the old or the new document. When writing or renaming fails, the temporary file is removed and the old file stays as it was.
  • No accidental files. When .smartconfig.json did not exist at construction, the write is refused unless you pass { createIfMissing: true }. The project directory itself is never created.
  • data must serialize to JSON; undefined (or anything JSON.stringify drops at the top level) is refused, as are an empty tool name and __proto__.

2. KeyValueStore<T> — Persistent Data Storage

A generic, typed key-value store that persists JSON to disk (or stays in-memory for tests). Supports change detection via RxJS observables.

import { KeyValueStore } from '@push.rocks/smartconfig';

interface Settings {
  username: string;
  theme: 'light' | 'dark';
}

// Store in ~/.smartconfig/kv/
const kv = new KeyValueStore<Settings>({
  typeArg: 'userHomeDir',
  identityArg: 'myApp',
  mandatoryKeys: ['username'],
});

await kv.writeKey('username', 'jane');
await kv.writeKey('theme', 'dark');

const user = await kv.readKey('username'); // 'jane'
const all = await kv.readAll();            // { username: 'jane', theme: 'dark' }

// React to changes
kv.changeSubject.subscribe((data) => console.log('changed:', data));

Storage types:

typeArg Where it goes Use case
'userHomeDir' ~/.smartconfig/kv/<identity>.json CLI tools, per-user state
'custom' Your path (file or directory) App data, project-local state
'ephemeral' Memory only — nothing on disk Tests

Methods:

Method Description
readKey(key) Read a single value
writeKey(key, value) Write a single value
readAll() Read everything
writeAll(obj) Merge an object into the store
deleteKey(key) Remove a key
reset() Wipe all keys (synced to disk)
wipe() Delete the backing file entirely
getMissingMandatoryKeys() Returns keys declared mandatory but not yet set
waitForKeysPresent(keys) Returns a Promise that resolves once all listed keys exist
waitForAndGetKey(key) Waits for a key, then returns its value

SSH-Agent Encrypted Single Values

For Node.js CLI tools, individual KeyValueStore values can be stored as encrypted JSON envelopes and unlocked later through a matching SSH key loaded in ssh-agent. This API is intentionally exposed from the Node-only subpath @push.rocks/smartconfig/sshagent so browser bundles do not pull in node:net, node:fs, or node:crypto.

import { KeyValueStore } from '@push.rocks/smartconfig';
import { SmartconfigSshAgentSecrets } from '@push.rocks/smartconfig/sshagent';

const kv = new KeyValueStore({
  typeArg: 'userHomeDir',
  identityArg: 'my-tool',
});

await SmartconfigSshAgentSecrets.writeKey(kv, 'apiToken', 'secret-token', {
  authorizedKeyPath: '~/.ssh/id_ed25519.pub',
});

const apiToken = await SmartconfigSshAgentSecrets.readKey(kv, 'apiToken');

The stored value is a single self-describing object. Other keys in the same config can remain plain JSON values.

{
  "apiUrl": "https://api.example.test",
  "apiToken": {
    "__smartconfigSecret": "ssh-agent-sign-v1",
    "keyHint": {
      "format": "openssh-authorized-key-v1",
      "authorizedKey": "ssh-ed25519 AAAAC3... user@host",
      "fingerprint": "SHA256:..."
    },
    "payload": {
      "encoding": "utf8"
    },
    "kdf": {
      "name": "HKDF-SHA256",
      "salt": "..."
    },
    "box": {
      "alg": "AES-256-GCM",
      "nonce": "...",
      "ciphertext": "...",
      "tag": "..."
    }
  }
}

The encrypted value includes an OpenSSH authorized-key-style hint. Smartconfig uses the hint's public key blob and SHA256: fingerprint to find the matching identity in ssh-agent; the key comment is not trusted for matching.

This is ssh-agent-signature-derived encryption, not public-key encryption. Encryption and decryption both require the matching ssh-ed25519 key to be available through ssh-agent. Anyone who can access your SSH_AUTH_SOCK can ask the agent to sign the unlock challenge, so this protects config values at rest on disk, not an already-compromised unlocked session.


3. AppData<T> — Environment Variable Mapping 🌟

The flagship class. Maps environment variables (or hardcoded values) into a typed config object with automatic type conversions, nested object support, and smart storage path selection.

import { AppData } from '@push.rocks/smartconfig';

interface Config {
  apiUrl: string;
  apiKey: string;
  features: {
    analytics: boolean;
    payment: boolean;
  };
  redis: {
    host: string;
    password: string;
  };
}

const appData = await AppData.createAndInit<Config>({
  requiredKeys: ['apiKey'],
  envMapping: {
    apiUrl: 'API_URL',                        // plain env var
    apiKey: 'hard:dev-key-123',               // hardcoded fallback
    features: {
      analytics: 'boolean:ENABLE_ANALYTICS',  // converts "true"/"false" → boolean
      payment: 'hard_boolean:true',           // hardcoded boolean
    },
    redis: {
      host: 'REDIS_HOST',
      password: 'base64:REDIS_PASSWORD_B64',  // base64-decode at load time
    },
  },
  overwriteObject: {
    apiUrl: 'http://localhost:3000',           // force override after env mapping
  },
});

const store = await appData.getKvStore();
const url = await store.readKey('apiUrl');

Mapping Prefixes

Prefix What it does Example mapping Result
(none) Raw env var as string 'MY_VAR' process.env.MY_VAR
hard: Hardcoded string 'hard:hello' "hello"
boolean: Env var → true/false 'boolean:FLAG' true or false
json: Env var → JSON.parse() 'json:CONFIG' parsed object
base64: Env var → base64 decode 'base64:SECRET' decoded string
hard_boolean: Hardcoded boolean 'hard_boolean:false' false
hard_json: Hardcoded JSON 'hard_json:{"a":1}' { a: 1 }
hard_base64: Hardcoded base64 'hard_base64:SGVsbG8=' "Hello"

Suffix detection also works: a mapping ending in _JSON or _BASE64 triggers the corresponding transform automatically.

Boolean Conversion Rules

The boolean: prefix (and hard_boolean:) recognizes:

  • true: "true", "1", "yes", "y", "on" (case-insensitive)
  • false: "false", "0", "no", "n", "off" (case-insensitive)

Nested Objects

Mapping values can be objects — they are resolved recursively:

envMapping: {
  database: {
    host: 'DB_HOST',
    port: 'hard:5432',
    credentials: {
      user: 'DB_USER',
      password: 'base64:DB_PASS_B64',
      ssl: 'boolean:DB_SSL',
    },
  },
}

Smart Storage Path

When no dirPath is specified, AppData auto-selects:

  1. /app/data — if it exists (containers)
  2. /data — if it exists (alternate container path)
  3. .nogit/appdata — local dev fallback

Or pass ephemeral: true for zero disk I/O (great for tests).

Static Helpers

Quick one-shot env var reads without creating an AppData instance:

const isEnabled = await AppData.valueAsBoolean('FEATURE_FLAG');
const config    = await AppData.valueAsJson<MyType>('CONFIG_JSON');
const secret    = await AppData.valueAsBase64('ENCODED_SECRET');
const url       = await AppData.valueAsString('API_URL');
const port      = await AppData.valueAsNumber('PORT');

Instance Methods

Method Description
getKvStore() Returns the underlying KeyValueStore<T>
logMissingKeys() Logs and returns any required keys that are missing
waitForAndGetKey(key) Blocks until a key is present, then returns it

Security 🔐

AppData diagnostics report configuration key names, environment-variable names, source kinds, transforms, presence and value types, never configuration values. This applies to ordinary-looking names as well as credentials, nested values, hardcoded literals, boolean conversions and overrides. No prefixes or previews of secret values are emitted. Mapping-failure diagnostics identify the affected key without logging arbitrary exception messages or objects, which may contain values. Keep key names, environment-variable names and storage paths free of secrets.

This logging guarantee does not encrypt stored values or change configuration results. AppData persists evaluated values by default; use ephemeral: true for runtime secrets that must stay in memory. Previously exposed credentials must be rotated or revoked in their owning service; upgrading the logger does not invalidate credentials or remove historical logs.


Real-World Example 🌍

import { Smartconfig, AppData, KeyValueStore } from '@push.rocks/smartconfig';

interface CliConfig {
  githubToken: string;
  model: 'gpt-3' | 'gpt-4';
  cache: { enabled: boolean; ttl: number };
}

// Tool-level config from .smartconfig.json
const sc = new Smartconfig();
const toolDefaults = sc.dataFor('mycli', { defaultModel: 'gpt-3' });

// Env-mapped runtime config
const appData = await AppData.createAndInit<CliConfig>({
  requiredKeys: ['githubToken'],
  envMapping: {
    githubToken: 'GITHUB_TOKEN',
    model: 'hard:gpt-4',
    cache: {
      enabled: 'boolean:ENABLE_CACHE',
      ttl: 'hard:3600',
    },
  },
});

// Persistent user-level cache
const cache = new KeyValueStore({
  typeArg: 'userHomeDir',
  identityArg: 'mycli-cache',
});

// Check mandatory keys
const missing = await appData.logMissingKeys();
if (missing.length > 0) {
  console.error('Missing config — set these env vars and retry.');
  process.exit(1);
}

const store = await appData.getKvStore();
const settings = await store.readAll();
console.log(`Model: ${settings.model}, Cache: ${settings.cache.enabled}`);

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
Enhances npm with additional configuration and tool management capabilities, including a key-value store for project setups.
Readme
1.7 MiB
Languages
TypeScript 100%