CLI Tooling
The configuration tooling API provides four programmatic operations — validate, diff, scaffold, and trace — that can be run from the udiot CLI or called directly in scripts, CI pipelines, or test helpers. Each operation accepts a ConfigToolingContext that describes where your schemas live and which environment to target.
Key Concepts
- Schema path — a path to a TypeScript or JavaScript module that exports one or more
@AIConfigclasses. The tooling imports the module, finds all decorated classes, and builds schemas from them. - Environment — the active environment name (
development,staging,production, etc.) used to locate the correct manifest file and to label output. - Workspace root — the root directory of your project. Used to resolve
configDirand.envfile paths.
ConfigToolingContext
All four functions accept (a superset of) this base context:
interface ConfigToolingContext {
schemaPath: string; // Path to the module exporting @AIConfig classes
environment: string; // e.g. 'production'
workspaceRoot: string; // Absolute path to the project root
configDir?: string; // Override default config/ directory
manifestFile?: string; // Explicit path to a manifest file
overrides?: Record<string, unknown>; // Additional overrides applied on top
secretProvider?: SecretProvider; // Custom secret provider
}
validateEnvironmentConfig
Loads configuration for the given environment and returns a validation result. A result is considered valid when there are no missing required keys and no policy violations.
import { validateEnvironmentConfig } from '@tagna/udiot';
const result = await validateEnvironmentConfig({
schemaPath: './src/config/index.ts',
environment: 'production',
workspaceRoot: process.cwd(),
});
// result extends ConfigLoadResult with an additional 'valid' flag
interface ValidationResult extends ConfigLoadResult {
valid: boolean; // true when missingRequired and policyViolations are both empty
}
CLI
udiot config validate --schema ./src/config/index.ts --env production
diffEnvironmentConfig
Compares the current resolved configuration against a baseline snapshot. The baseline can be a JSON file or a previously saved release snapshot.
import { diffEnvironmentConfig } from '@tagna/udiot';
const result = await diffEnvironmentConfig({
schemaPath: './src/config/index.ts',
environment: 'staging',
workspaceRoot: process.cwd(),
compareFile: './config/.snapshots/staging.json', // optional
compareLabel: 'last-release', // alternative to compareFile
});
DiffResult
interface DiffResult {
changes: DiffEntry[];
current: Record<string, unknown>; // Flattened current values
baseline?: Record<string, unknown>; // Flattened baseline values (undefined if no baseline)
}
interface DiffEntry {
key: string; // Fully-qualified dotted key
from?: unknown; // Previous value (undefined for 'added' entries)
to?: unknown; // New value (undefined for 'removed' entries)
type: 'added' | 'removed' | 'changed';
}
When compareLabel is 'last-release', the tool looks for a file at {configDir}/.snapshots/{environment}.json.
CLI
udiot config diff --schema ./src/config/index.ts --env staging --compare last-release
scaffoldEnvironmentFiles
Generates a .env.{environment} file and a YAML manifest template from your schemas. Sensitive fields are included in the .env output with an empty value (never with defaults). Non-sensitive fields include their default value.
import { scaffoldEnvironmentFiles } from '@tagna/udiot';
const result = await scaffoldEnvironmentFiles({
schemaPath: './src/config/index.ts',
environment: 'staging',
workspaceRoot: process.cwd(),
withDocs: true, // Include field descriptions as comments
force: false, // Set to true to overwrite existing files
});
interface ScaffoldResult {
envFile: string; // Absolute path of the written .env file
manifestFile: string; // Absolute path of the written YAML manifest
envCreated: boolean; // false if the file already existed and was overwritten
manifestCreated: boolean;
envContent: string; // Generated .env content
manifestContent: string; // Generated YAML content
}
The generated .env.staging looks like:
# Generated by udiot config scaffold
# Profiles: local/staging/prod
# Namespace: database
# Database host name
# Required
DATABASE_HOST=
DATABASE_PORT=5432
# Required
DATABASE_PASSWORD=
DATABASE_MAX_CONNECTIONS=10
Sensitive fields have an empty value regardless of any default set in the schema.
CLI
udiot config scaffold --schema ./src/config/index.ts --env staging --with-docs
traceConfigKey
Shows the full provenance history for a single configuration key: which layers set it and what value each layer provided (sensitive values are masked).
import { traceConfigKey } from '@tagna/udiot';
const result = await traceConfigKey({
schemaPath: './src/config/index.ts',
environment: 'production',
workspaceRoot: process.cwd(),
key: 'database.host',
});
interface TraceResult {
key: string;
entries: ConfigProvenanceEntry[];
}
Each entry in entries contains { key, value, source, layer, timestamp, sensitive }.
CLI
udiot config trace database.host --schema ./dist/config/index.js --env production
Example output:
Tracing: database.host
[schema-defaults] = undefined (schema-default)
[environment] = db.example.com (env:DATABASE_HOST)
✓ Winning value: db.example.com (from environment)
Internal schema loading
loadSchemasFromPath is an internal helper, not a root package export.
Use the exported configuration tooling functions below or the CLI. Prefer
compiled ESM schema files; TypeScript source loading requires an appropriate
loader and is not equivalent to a normal production import.
registerSchemaLoader
Replaces the default dynamic-import loader with a custom function. Useful in environments where import() cannot load arbitrary paths (e.g., bundled outputs or custom build systems).
import { registerSchemaLoader } from '@tagna/udiot';
registerSchemaLoader(async (schemaPath) => {
const module = await myCustomLoader(schemaPath);
return extractSchemasFromModule(module);
});
// Pass null to restore the default loader
registerSchemaLoader(null);
Basic Usage Example — CI Validation
// scripts/validate-config.ts
import { validateEnvironmentConfig } from '@tagna/udiot';
async function main() {
const result = await validateEnvironmentConfig({
schemaPath: './src/config/index.ts',
environment: process.env.DEPLOY_ENV ?? 'production',
workspaceRoot: process.cwd(),
});
if (!result.valid) {
console.error('Configuration validation failed:');
for (const key of result.missingRequired) {
console.error(` MISSING REQUIRED: ${key}`);
}
for (const v of result.policyViolations) {
console.error(` POLICY VIOLATION: ${v.key} — ${v.reason} (expected ${v.expected}, got ${v.actual})`);
}
process.exit(1);
}
console.log('Configuration is valid.');
}
main();
Run this in your CI pipeline before deploying:
npx ts-node scripts/validate-config.ts
Common Patterns
Generating templates for a new environment
# Create staging config files with docs
udiot config scaffold \
--schema ./src/config/index.ts \
--env staging \
--with-docs
Commit the generated YAML manifest. Keep the .env.staging in .gitignore and populate it using your secrets manager.
Detecting drift before a release
udiot config diff \
--schema ./src/config/index.ts \
--env production \
--compare last-release
This reveals new keys added to the schema that are not yet in the production environment, or keys removed from the schema that still exist in the environment.
Integration Notes
- The tooling functions are thin wrappers over
EnvironmentConfigManager. They load, validate, and return results without side effects onprocess.envor the file system (exceptscaffoldEnvironmentFiles, which writes files). diffEnvironmentConfigcompares resolved values (after type casting), not raw strings. A value of"5432"in the environment and5432as the schema default will appear aschangedbecause the baseline may have stored the number.
Pitfalls
scaffoldEnvironmentFilesthrows if the output files already exist andforceis not set. This prevents accidental overwriting of handcrafted.envfiles.- When using
compareLabel: 'last-release', the snapshot file must be created manually or by a release script. The tooling does not write snapshots automatically. - Loading
.tsschemas requirests-nodeat runtime. In production environments where TypeScript is pre-compiled, pointschemaPathto the compiled.jsfile instead.