@outfitter/config 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Port number schema (1-65535) with string-to-number coercion.
4
+ *
5
+ * Validates that a string contains only digits, then transforms
6
+ * to a number and validates the port range.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const schema = z.object({ PORT: portSchema });
11
+ * schema.parse({ PORT: "3000" }); // { PORT: 3000 }
12
+ * schema.parse({ PORT: "invalid" }); // throws
13
+ * schema.parse({ PORT: "99999" }); // throws (out of range)
14
+ * ```
15
+ */
16
+ declare const portSchema: z.ZodType<number, string>;
17
+ /**
18
+ * Boolean schema with proper string coercion.
19
+ *
20
+ * Accepts: "true", "false", "1", "0", ""
21
+ * - "true" or "1" -> true
22
+ * - "false", "0", or "" -> false
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const schema = z.object({ DEBUG: booleanSchema });
27
+ * schema.parse({ DEBUG: "true" }); // { DEBUG: true }
28
+ * schema.parse({ DEBUG: "1" }); // { DEBUG: true }
29
+ * schema.parse({ DEBUG: "false" }); // { DEBUG: false }
30
+ * schema.parse({ DEBUG: "" }); // { DEBUG: false }
31
+ * ```
32
+ */
33
+ declare const booleanSchema: z.ZodType<boolean, string>;
34
+ /**
35
+ * Optional boolean schema - returns undefined if not set.
36
+ *
37
+ * Unlike `booleanSchema`, this returns `undefined` for missing
38
+ * or empty values, allowing callers to distinguish between
39
+ * "explicitly set to false" and "not set".
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const schema = z.object({ NO_COLOR: optionalBooleanSchema });
44
+ * schema.parse({ NO_COLOR: "true" }); // { NO_COLOR: true }
45
+ * schema.parse({ NO_COLOR: "" }); // { NO_COLOR: undefined }
46
+ * schema.parse({ NO_COLOR: undefined }); // { NO_COLOR: undefined }
47
+ * ```
48
+ */
49
+ declare const optionalBooleanSchema: z.ZodType<boolean | undefined, string | undefined>;
50
+ /**
51
+ * Parse and validate environment variables against a Zod schema.
52
+ *
53
+ * By default reads from `process.env`, but accepts a custom env
54
+ * object for testing.
55
+ *
56
+ * @typeParam T - The Zod schema shape
57
+ * @param schema - Zod object schema to validate against
58
+ * @param envObj - Environment object (defaults to process.env)
59
+ * @returns Validated and transformed environment object
60
+ * @throws {z.ZodError} When validation fails
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const AppEnv = z.object({
65
+ * PORT: portSchema,
66
+ * DEBUG: booleanSchema,
67
+ * });
68
+ *
69
+ * const env = parseEnv(AppEnv);
70
+ * console.log(env.PORT); // number
71
+ * console.log(env.DEBUG); // boolean
72
+ * ```
73
+ */
74
+ declare function parseEnv<T extends z.ZodRawShape>(schema: z.ZodObject<T>, envObj?: Record<string, string | undefined>): z.infer<z.ZodObject<T>>;
75
+ type AppEnvShape = {
76
+ NODE_ENV: z.ZodDefault<z.ZodEnum<{
77
+ development: "development";
78
+ test: "test";
79
+ production: "production";
80
+ }>>;
81
+ NO_COLOR: typeof optionalBooleanSchema;
82
+ FORCE_COLOR: typeof optionalBooleanSchema;
83
+ CI: typeof optionalBooleanSchema;
84
+ TERM: z.ZodOptional<z.ZodString>;
85
+ XDG_CONFIG_HOME: z.ZodOptional<z.ZodString>;
86
+ XDG_DATA_HOME: z.ZodOptional<z.ZodString>;
87
+ XDG_STATE_HOME: z.ZodOptional<z.ZodString>;
88
+ XDG_CACHE_HOME: z.ZodOptional<z.ZodString>;
89
+ HOME: z.ZodOptional<z.ZodString>;
90
+ };
91
+ /**
92
+ * Schema for common application environment variables.
93
+ */
94
+ declare const appEnvSchema: z.ZodObject<AppEnvShape>;
95
+ /**
96
+ * Type for the pre-parsed application environment.
97
+ */
98
+ type Env = z.infer<typeof appEnvSchema>;
99
+ /**
100
+ * Pre-parsed application environment.
101
+ *
102
+ * Access common environment variables with proper typing:
103
+ * - `env.NODE_ENV`: "development" | "test" | "production"
104
+ * - `env.NO_COLOR`: boolean | undefined
105
+ * - `env.FORCE_COLOR`: boolean | undefined
106
+ * - `env.CI`: boolean | undefined
107
+ * - `env.TERM`: string | undefined
108
+ * - `env.XDG_*`: string | undefined
109
+ * - `env.HOME`: string | undefined
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * import { env } from "@outfitter/config";
114
+ *
115
+ * if (env.CI) {
116
+ * console.log("Running in CI environment");
117
+ * }
118
+ *
119
+ * if (env.NO_COLOR) {
120
+ * // Disable color output
121
+ * }
122
+ * ```
123
+ */
124
+ declare const env: Env;
125
+ /**
126
+ * Reads an optional boolean from process.env at call time.
127
+ *
128
+ * Unlike `env.NO_COLOR` (which is static), this reads dynamically
129
+ * for use cases where env vars may change at runtime (e.g., tests).
130
+ *
131
+ * @param key - The environment variable name to read
132
+ * @returns `true` if "true"/"1", `false` if "false"/"0", `undefined` otherwise
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * // For terminal detection that needs dynamic behavior
137
+ * if (getEnvBoolean("NO_COLOR")) {
138
+ * // colors disabled
139
+ * }
140
+ * ```
141
+ */
142
+ declare function getEnvBoolean(key: "NO_COLOR" | "FORCE_COLOR" | "CI"): boolean | undefined;
143
+ import { TaggedErrorClass } from "@outfitter/contracts";
144
+ import { NotFoundError, Result, ValidationError } from "@outfitter/contracts";
145
+ import { ZodSchema } from "zod";
146
+ type ParseErrorFields = {
147
+ /** Human-readable error message describing the parse failure */
148
+ message: string;
149
+ /** Name of the file that failed to parse */
150
+ filename: string;
151
+ /** Line number where the error occurred (if available) */
152
+ line?: number;
153
+ /** Column number where the error occurred (if available) */
154
+ column?: number;
155
+ };
156
+ declare const ParseErrorBase: TaggedErrorClass<"ParseError", ParseErrorFields>;
157
+ /**
158
+ * Error thrown when a configuration file cannot be parsed.
159
+ *
160
+ * Contains details about the parse failure including the filename
161
+ * and optionally the line/column where the error occurred.
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const result = parseConfigFile("invalid toml [", "config.toml");
166
+ * if (result.isErr() && result.error._tag === "ParseError") {
167
+ * console.error(`Parse error in ${result.error.filename}: ${result.error.message}`);
168
+ * }
169
+ * ```
170
+ */
171
+ declare class ParseError extends ParseErrorBase {
172
+ readonly category: "validation";
173
+ }
174
+ /**
175
+ * Get the XDG config directory for an application.
176
+ *
177
+ * Uses `XDG_CONFIG_HOME` if set, otherwise defaults to `~/.config`.
178
+ * This follows the XDG Base Directory Specification for storing
179
+ * user-specific configuration files.
180
+ *
181
+ * @param appName - Application name used as subdirectory
182
+ * @returns Absolute path to the application's config directory
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * // With XDG_CONFIG_HOME="/custom/config"
187
+ * getConfigDir("myapp"); // "/custom/config/myapp"
188
+ *
189
+ * // Without XDG_CONFIG_HOME (uses default)
190
+ * getConfigDir("myapp"); // "/home/user/.config/myapp"
191
+ * ```
192
+ */
193
+ declare function getConfigDir(appName: string): string;
194
+ /**
195
+ * Get the XDG data directory for an application.
196
+ *
197
+ * Uses `XDG_DATA_HOME` if set, otherwise defaults to `~/.local/share`.
198
+ * This follows the XDG Base Directory Specification for storing
199
+ * user-specific data files (databases, generated content, etc.).
200
+ *
201
+ * @param appName - Application name used as subdirectory
202
+ * @returns Absolute path to the application's data directory
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * // With XDG_DATA_HOME="/custom/data"
207
+ * getDataDir("myapp"); // "/custom/data/myapp"
208
+ *
209
+ * // Without XDG_DATA_HOME (uses default)
210
+ * getDataDir("myapp"); // "/home/user/.local/share/myapp"
211
+ * ```
212
+ */
213
+ declare function getDataDir(appName: string): string;
214
+ /**
215
+ * Get the XDG cache directory for an application.
216
+ *
217
+ * Uses `XDG_CACHE_HOME` if set, otherwise defaults to `~/.cache`.
218
+ * This follows the XDG Base Directory Specification for storing
219
+ * non-essential cached data that can be regenerated.
220
+ *
221
+ * @param appName - Application name used as subdirectory
222
+ * @returns Absolute path to the application's cache directory
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * // With XDG_CACHE_HOME="/custom/cache"
227
+ * getCacheDir("myapp"); // "/custom/cache/myapp"
228
+ *
229
+ * // Without XDG_CACHE_HOME (uses default)
230
+ * getCacheDir("myapp"); // "/home/user/.cache/myapp"
231
+ * ```
232
+ */
233
+ declare function getCacheDir(appName: string): string;
234
+ /**
235
+ * Get the XDG state directory for an application.
236
+ *
237
+ * Uses `XDG_STATE_HOME` if set, otherwise defaults to `~/.local/state`.
238
+ * This follows the XDG Base Directory Specification for storing
239
+ * state data that should persist between restarts (logs, history, etc.).
240
+ *
241
+ * @param appName - Application name used as subdirectory
242
+ * @returns Absolute path to the application's state directory
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // With XDG_STATE_HOME="/custom/state"
247
+ * getStateDir("myapp"); // "/custom/state/myapp"
248
+ *
249
+ * // Without XDG_STATE_HOME (uses default)
250
+ * getStateDir("myapp"); // "/home/user/.local/state/myapp"
251
+ * ```
252
+ */
253
+ declare function getStateDir(appName: string): string;
254
+ /**
255
+ * Deep merge two objects with configurable merge semantics.
256
+ *
257
+ * Merge behavior:
258
+ * - Recursively merges nested plain objects
259
+ * - Arrays are replaced (not concatenated)
260
+ * - `null` explicitly replaces the target value
261
+ * - `undefined` is skipped (does not override)
262
+ *
263
+ * @typeParam T - The type of the target object
264
+ * @param target - Base object to merge into (not mutated)
265
+ * @param source - Object with values to merge
266
+ * @returns New object with merged values
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * const defaults = { server: { port: 3000, host: "localhost" } };
271
+ * const overrides = { server: { port: 8080 } };
272
+ *
273
+ * const merged = deepMerge(defaults, overrides);
274
+ * // { server: { port: 8080, host: "localhost" } }
275
+ * ```
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * // Arrays replace, not merge
280
+ * const target = { tags: ["a", "b"] };
281
+ * const source = { tags: ["c"] };
282
+ * deepMerge(target, source); // { tags: ["c"] }
283
+ *
284
+ * // undefined is skipped
285
+ * const base = { a: 1, b: 2 };
286
+ * deepMerge(base, { a: undefined, b: 3 }); // { a: 1, b: 3 }
287
+ *
288
+ * // null explicitly replaces
289
+ * deepMerge(base, { a: null }); // { a: null, b: 2 }
290
+ * ```
291
+ */
292
+ declare function deepMerge<T extends object>(target: T, source: Partial<T>): T;
293
+ /**
294
+ * Parse configuration file content based on filename extension.
295
+ *
296
+ * Supports multiple formats:
297
+ * - `.toml` - Parsed with smol-toml (preferred for config)
298
+ * - `.yaml`, `.yml` - Parsed with yaml (merge key support enabled)
299
+ * - `.json` - Parsed with strict JSON.parse
300
+ * - `.json5` - Parsed with json5 (comments and trailing commas allowed)
301
+ *
302
+ * @param content - Raw file content to parse
303
+ * @param filename - Filename used to determine format (by extension)
304
+ * @returns Result containing parsed object or ParseError
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * const toml = `
309
+ * [server]
310
+ * port = 3000
311
+ * host = "localhost"
312
+ * `;
313
+ *
314
+ * const result = parseConfigFile(toml, "config.toml");
315
+ * if (result.isOk()) {
316
+ * console.log(result.value.server.port); // 3000
317
+ * }
318
+ * ```
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * // YAML with anchors/aliases
323
+ * const yaml = `
324
+ * defaults: &defaults
325
+ * timeout: 5000
326
+ * server:
327
+ * <<: *defaults
328
+ * port: 3000
329
+ * `;
330
+ *
331
+ * const result = parseConfigFile(yaml, "config.yaml");
332
+ * if (result.isOk()) {
333
+ * console.log(result.value.server.timeout); // 5000
334
+ * }
335
+ * ```
336
+ */
337
+ declare function parseConfigFile(content: string, filename: string): Result<Record<string, unknown>, InstanceType<typeof ParseError>>;
338
+ /**
339
+ * Configuration sources for multi-layer resolution.
340
+ *
341
+ * Sources are merged in precedence order (lowest to highest):
342
+ * `defaults` < `file` < `env` < `flags`
343
+ *
344
+ * @typeParam T - The configuration type
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * const sources: ConfigSources<AppConfig> = {
349
+ * defaults: { timeout: 5000, debug: false },
350
+ * file: loadedFromDisk,
351
+ * env: { timeout: parseInt(process.env.TIMEOUT!) },
352
+ * flags: { debug: cliArgs.debug },
353
+ * };
354
+ * ```
355
+ */
356
+ interface ConfigSources<T> {
357
+ /** Default values (lowest precedence) */
358
+ defaults?: Partial<T>;
359
+ /** Values loaded from config file */
360
+ file?: Partial<T>;
361
+ /** Values from environment variables */
362
+ env?: Partial<T>;
363
+ /** CLI flag values (highest precedence) */
364
+ flags?: Partial<T>;
365
+ }
366
+ /**
367
+ * Resolve configuration from multiple sources with precedence.
368
+ *
369
+ * Merges sources in order: `defaults` < `file` < `env` < `flags`.
370
+ * Higher precedence sources override lower ones. Nested objects
371
+ * are deep-merged; arrays are replaced.
372
+ *
373
+ * The merged result is validated against the provided Zod schema.
374
+ *
375
+ * @typeParam T - The configuration type (inferred from schema)
376
+ * @param schema - Zod schema for validation
377
+ * @param sources - Configuration sources to merge
378
+ * @returns Result containing validated config or ValidationError/ParseError
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const AppSchema = z.object({
383
+ * port: z.number().min(1).max(65535),
384
+ * host: z.string(),
385
+ * debug: z.boolean().default(false),
386
+ * });
387
+ *
388
+ * const result = resolveConfig(AppSchema, {
389
+ * defaults: { port: 3000, host: "localhost" },
390
+ * file: { port: 8080 },
391
+ * env: { debug: true },
392
+ * flags: { port: 9000 },
393
+ * });
394
+ *
395
+ * if (result.isOk()) {
396
+ * // { port: 9000, host: "localhost", debug: true }
397
+ * console.log(result.value);
398
+ * }
399
+ * ```
400
+ */
401
+ declare function resolveConfig<T>(schema: ZodSchema<T>, sources: ConfigSources<T>): Result<T, InstanceType<typeof ValidationError> | InstanceType<typeof ParseError>>;
402
+ /**
403
+ * Options for the {@link loadConfig} function.
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * const options: LoadConfigOptions = {
408
+ * searchPaths: ["/etc/myapp", "/opt/myapp/config"],
409
+ * };
410
+ * ```
411
+ */
412
+ interface LoadConfigOptions {
413
+ /**
414
+ * Custom search paths to check for config files.
415
+ * When provided, overrides the default XDG-based search paths.
416
+ * Paths are searched in order; first match wins.
417
+ */
418
+ searchPaths?: string[];
419
+ }
420
+ /**
421
+ * Load configuration for an application from XDG-compliant paths.
422
+ *
423
+ * Search order (first found wins):
424
+ * 1. Custom `searchPaths` if provided in options
425
+ * 2. `$XDG_CONFIG_HOME/{appName}/config.{ext}`
426
+ * 3. `~/.config/{appName}/config.{ext}`
427
+ *
428
+ * File format preference: `.toml` > `.yaml` > `.yml` > `.json` > `.json5`
429
+ *
430
+ * @typeParam T - The configuration type (inferred from schema)
431
+ * @param appName - Application name for XDG directory lookup
432
+ * @param schema - Zod schema for validation
433
+ * @param options - Optional configuration (custom search paths)
434
+ * @returns Result containing validated config or NotFoundError/ValidationError/ParseError
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * import { loadConfig } from "@outfitter/config";
439
+ * import { z } from "zod";
440
+ *
441
+ * const AppConfigSchema = z.object({
442
+ * apiKey: z.string(),
443
+ * timeout: z.number().default(5000),
444
+ * features: z.object({
445
+ * darkMode: z.boolean().default(false),
446
+ * }),
447
+ * });
448
+ *
449
+ * // Searches ~/.config/myapp/config.{toml,yaml,json,...}
450
+ * const result = await loadConfig("myapp", AppConfigSchema);
451
+ *
452
+ * if (result.isOk()) {
453
+ * console.log("API Key:", result.value.apiKey);
454
+ * console.log("Timeout:", result.value.timeout);
455
+ * } else {
456
+ * console.error("Failed to load config:", result.error.message);
457
+ * }
458
+ * ```
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * // With custom search paths
463
+ * const result = await loadConfig("myapp", AppConfigSchema, {
464
+ * searchPaths: ["/etc/myapp", "/opt/myapp/config"],
465
+ * });
466
+ * ```
467
+ */
468
+ declare function loadConfig<T>(appName: string, schema: ZodSchema<T>, options?: LoadConfigOptions): Result<T, InstanceType<typeof NotFoundError> | InstanceType<typeof ValidationError> | InstanceType<typeof ParseError>>;
469
+ export { resolveConfig, portSchema, parseEnv, parseConfigFile, optionalBooleanSchema, loadConfig, getStateDir, getEnvBoolean, getDataDir, getConfigDir, getCacheDir, env, deepMerge, booleanSchema, ParseError, LoadConfigOptions, Env, ConfigSources };