@utilarium/cardigantime 0.0.24-dev.0

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.
Files changed (89) hide show
  1. package/LICENSE +65 -0
  2. package/README.md +398 -0
  3. package/dist/cardigantime.cjs +2169 -0
  4. package/dist/cardigantime.cjs.map +1 -0
  5. package/dist/cardigantime.d.ts +92 -0
  6. package/dist/cardigantime.js +198 -0
  7. package/dist/cardigantime.js.map +1 -0
  8. package/dist/config/executable-security.d.ts +32 -0
  9. package/dist/config/format-detector.d.ts +59 -0
  10. package/dist/configure.d.ts +55 -0
  11. package/dist/configure.js +125 -0
  12. package/dist/configure.js.map +1 -0
  13. package/dist/constants.d.ts +25 -0
  14. package/dist/constants.js +38 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/discovery/discoverer.d.ts +62 -0
  17. package/dist/discovery/hierarchical-modes.d.ts +64 -0
  18. package/dist/discovery/index.d.ts +15 -0
  19. package/dist/discovery/patterns.d.ts +77 -0
  20. package/dist/discovery/root-detection.d.ts +100 -0
  21. package/dist/discovery/traversal-security.d.ts +106 -0
  22. package/dist/env/errors.d.ts +18 -0
  23. package/dist/env/index.d.ts +7 -0
  24. package/dist/env/naming.d.ts +38 -0
  25. package/dist/env/parser.d.ts +61 -0
  26. package/dist/env/reader.d.ts +45 -0
  27. package/dist/env/resolver.d.ts +25 -0
  28. package/dist/env/schema-utils.d.ts +33 -0
  29. package/dist/env/types.d.ts +43 -0
  30. package/dist/error/ArgumentError.d.ts +31 -0
  31. package/dist/error/ArgumentError.js +48 -0
  32. package/dist/error/ArgumentError.js.map +1 -0
  33. package/dist/error/ConfigParseError.d.ts +26 -0
  34. package/dist/error/ConfigurationError.d.ts +21 -0
  35. package/dist/error/ConfigurationError.js +46 -0
  36. package/dist/error/ConfigurationError.js.map +1 -0
  37. package/dist/error/FileSystemError.d.ts +30 -0
  38. package/dist/error/FileSystemError.js +58 -0
  39. package/dist/error/FileSystemError.js.map +1 -0
  40. package/dist/error/index.d.ts +4 -0
  41. package/dist/mcp/discovery.d.ts +105 -0
  42. package/dist/mcp/errors.d.ts +75 -0
  43. package/dist/mcp/index.d.ts +22 -0
  44. package/dist/mcp/integration.d.ts +184 -0
  45. package/dist/mcp/parser.d.ts +141 -0
  46. package/dist/mcp/resolver.d.ts +165 -0
  47. package/dist/mcp/tools/check-config-types.d.ts +208 -0
  48. package/dist/mcp/tools/check-config.d.ts +85 -0
  49. package/dist/mcp/tools/index.d.ts +12 -0
  50. package/dist/mcp/types.d.ts +210 -0
  51. package/dist/parsers/index.d.ts +25 -0
  52. package/dist/parsers/javascript-parser.d.ts +12 -0
  53. package/dist/parsers/json-parser.d.ts +6 -0
  54. package/dist/parsers/typescript-parser.d.ts +15 -0
  55. package/dist/parsers/yaml-parser.d.ts +6 -0
  56. package/dist/read.d.ts +56 -0
  57. package/dist/read.js +653 -0
  58. package/dist/read.js.map +1 -0
  59. package/dist/security/audit-logger.d.ts +135 -0
  60. package/dist/security/cli-validator.d.ts +73 -0
  61. package/dist/security/config-validator.d.ts +95 -0
  62. package/dist/security/defaults.d.ts +17 -0
  63. package/dist/security/index.d.ts +14 -0
  64. package/dist/security/numeric-guard.d.ts +111 -0
  65. package/dist/security/path-guard.d.ts +53 -0
  66. package/dist/security/profiles.d.ts +127 -0
  67. package/dist/security/security-validator.d.ts +109 -0
  68. package/dist/security/string-guard.d.ts +92 -0
  69. package/dist/security/types.d.ts +126 -0
  70. package/dist/security/zod-secure-enum.d.ts +20 -0
  71. package/dist/security/zod-secure-number.d.ts +39 -0
  72. package/dist/security/zod-secure-path.d.ts +24 -0
  73. package/dist/security/zod-secure-string.d.ts +38 -0
  74. package/dist/types.d.ts +584 -0
  75. package/dist/types.js +56 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/util/hierarchical.d.ts +136 -0
  78. package/dist/util/hierarchical.js +436 -0
  79. package/dist/util/hierarchical.js.map +1 -0
  80. package/dist/util/schema-defaults.d.ts +80 -0
  81. package/dist/util/schema-defaults.js +118 -0
  82. package/dist/util/schema-defaults.js.map +1 -0
  83. package/dist/util/storage.d.ts +31 -0
  84. package/dist/util/storage.js +154 -0
  85. package/dist/util/storage.js.map +1 -0
  86. package/dist/validate.d.ts +113 -0
  87. package/dist/validate.js +260 -0
  88. package/dist/validate.js.map +1 -0
  89. package/package.json +84 -0
@@ -0,0 +1,2169 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
+
5
+ const yaml = require('js-yaml');
6
+ const path = require('node:path');
7
+ const fs = require('node:fs');
8
+ const glob = require('glob');
9
+ const crypto = require('node:crypto');
10
+ const zod = require('zod');
11
+
12
+ function _interopNamespaceDefault(e) {
13
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
14
+ if (e) {
15
+ for (const k in e) {
16
+ if (k !== 'default') {
17
+ const d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: () => e[k]
21
+ });
22
+ }
23
+ }
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ const yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml);
30
+ const path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
31
+ const fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
32
+ const crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
33
+
34
+ function _define_property$2(obj, key, value) {
35
+ if (key in obj) {
36
+ Object.defineProperty(obj, key, {
37
+ value: value,
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true
41
+ });
42
+ } else {
43
+ obj[key] = value;
44
+ }
45
+ return obj;
46
+ }
47
+ /**
48
+ * Error thrown when CLI arguments or function parameters are invalid.
49
+ *
50
+ * This error provides specific context about which argument failed validation
51
+ * and why, making it easier for users to fix their command-line usage or
52
+ * for developers to debug parameter issues.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * throw new ArgumentError('config-directory', 'Path cannot be empty');
57
+ * // Error message: "Path cannot be empty"
58
+ * // error.argument: "config-directory"
59
+ * ```
60
+ */ class ArgumentError extends Error {
61
+ /**
62
+ * Gets the name of the argument that caused this error.
63
+ *
64
+ * @returns The argument name
65
+ */ get argument() {
66
+ return this.argumentName;
67
+ }
68
+ /**
69
+ * Creates a new ArgumentError instance.
70
+ *
71
+ * @param argumentName - The name of the invalid argument
72
+ * @param message - Description of why the argument is invalid
73
+ */ constructor(argumentName, message){
74
+ super(`${message}`), /** The name of the argument that caused the error */ _define_property$2(this, "argumentName", void 0);
75
+ this.name = 'ArgumentError';
76
+ this.argumentName = argumentName;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Validates a configuration directory path to ensure it's safe and valid.
82
+ *
83
+ * Performs security and safety checks including:
84
+ * - Non-empty string validation
85
+ * - Null byte injection prevention
86
+ * - Path length validation
87
+ * - Type checking
88
+ *
89
+ * @param configDirectory - The configuration directory path to validate
90
+ * @param _testThrowNonArgumentError - Internal testing parameter to simulate non-ArgumentError exceptions
91
+ * @returns The trimmed and validated configuration directory path
92
+ * @throws {ArgumentError} When the directory path is invalid
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const validDir = validateConfigDirectory('./config'); // Returns './config'
97
+ * const invalidDir = validateConfigDirectory(''); // Throws ArgumentError
98
+ * ```
99
+ */ function validateConfigDirectory$2(configDirectory, _testThrowNonArgumentError) {
100
+ if (!configDirectory) {
101
+ throw new ArgumentError('configDirectory', 'Configuration directory cannot be empty');
102
+ }
103
+ if (typeof configDirectory !== 'string') {
104
+ throw new ArgumentError('configDirectory', 'Configuration directory must be a string');
105
+ }
106
+ const trimmed = configDirectory.trim();
107
+ if (trimmed.length === 0) {
108
+ throw new ArgumentError('configDirectory', 'Configuration directory cannot be empty or whitespace only');
109
+ }
110
+ // Check for obviously invalid paths
111
+ if (trimmed.includes('\0')) {
112
+ throw new ArgumentError('configDirectory', 'Configuration directory contains invalid null character');
113
+ }
114
+ // Validate path length (reasonable limit)
115
+ if (trimmed.length > 1000) {
116
+ throw new ArgumentError('configDirectory', 'Configuration directory path is too long (max 1000 characters)');
117
+ }
118
+ return trimmed;
119
+ }
120
+ /**
121
+ * Configures a Commander.js command with Cardigantime's CLI options.
122
+ *
123
+ * This function adds command-line options that allow users to override
124
+ * configuration settings at runtime, such as:
125
+ * - --config-directory: Override the default configuration directory
126
+ *
127
+ * The function validates both the command object and the options to ensure
128
+ * they meet the requirements for proper integration.
129
+ *
130
+ * @template T - The Zod schema shape type for configuration validation
131
+ * @param command - The Commander.js Command instance to configure
132
+ * @param options - Cardigantime options containing defaults and schema
133
+ * @param _testThrowNonArgumentError - Internal testing parameter
134
+ * @returns Promise resolving to the configured Command instance
135
+ * @throws {ArgumentError} When command or options are invalid
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * import { Command } from 'commander';
140
+ * import { configure } from './configure';
141
+ *
142
+ * const program = new Command();
143
+ * const configuredProgram = await configure(program, options);
144
+ *
145
+ * // Now the program accepts: --config-directory <path>
146
+ * ```
147
+ */ const configure = async (command, options, _testThrowNonArgumentError)=>{
148
+ // Validate the command object
149
+ if (!command) {
150
+ throw new ArgumentError('command', 'Command instance is required');
151
+ }
152
+ if (typeof command.option !== 'function') {
153
+ throw new ArgumentError('command', 'Command must be a valid Commander.js Command instance');
154
+ }
155
+ // Validate options
156
+ if (!options) {
157
+ throw new ArgumentError('options', 'Options object is required');
158
+ }
159
+ if (!options.defaults) {
160
+ throw new ArgumentError('options.defaults', 'Options must include defaults configuration');
161
+ }
162
+ if (!options.defaults.configDirectory) {
163
+ throw new ArgumentError('options.defaults.configDirectory', 'Default config directory is required');
164
+ }
165
+ // Validate the default config directory
166
+ const validatedDefaultDir = validateConfigDirectory$2(options.defaults.configDirectory);
167
+ let retCommand = command;
168
+ // Add the config directory option with validation
169
+ retCommand = retCommand.option('-c, --config-directory <configDirectory>', 'Configuration directory path', (value)=>{
170
+ try {
171
+ return validateConfigDirectory$2(value, _testThrowNonArgumentError);
172
+ } catch (error) {
173
+ if (error instanceof ArgumentError) {
174
+ // Re-throw with more specific context for CLI usage
175
+ throw new ArgumentError('config-directory', `Invalid --config-directory: ${error.message}`);
176
+ }
177
+ throw error;
178
+ }
179
+ }, validatedDefaultDir);
180
+ // Add the init config option
181
+ retCommand = retCommand.option('--init-config', 'Generate initial configuration file and exit');
182
+ // Add the check config option
183
+ retCommand = retCommand.option('--check-config', 'Display resolved configuration with source tracking and exit');
184
+ // Add the config format option
185
+ retCommand = retCommand.option('--config-format <format>', 'Force a specific configuration file format (yaml, json, javascript, typescript)', (value)=>{
186
+ const validFormats = [
187
+ 'yaml',
188
+ 'json',
189
+ 'javascript',
190
+ 'typescript'
191
+ ];
192
+ const normalized = value.toLowerCase();
193
+ if (!validFormats.includes(normalized)) {
194
+ throw new ArgumentError('config-format', `Invalid --config-format: must be one of ${validFormats.join(', ')}`);
195
+ }
196
+ return normalized;
197
+ });
198
+ return retCommand;
199
+ };
200
+
201
+ /** Version string populated at build time with git and system information */ const VERSION = '0.0.24-dev.0 (working/2325c27 2026-01-30 13:16:50 -0800) darwin arm64 v24.8.0';
202
+ /** The program name used in CLI help and error messages */ const PROGRAM_NAME = 'cardigantime';
203
+ /** Default file encoding for reading configuration files */ const DEFAULT_ENCODING = 'utf8';
204
+ /** Default configuration file name to look for in the config directory */ const DEFAULT_CONFIG_FILE = 'config.yaml';
205
+ /**
206
+ * Default configuration options applied when creating a Cardigantime instance.
207
+ * These provide sensible defaults that work for most use cases.
208
+ */ const DEFAULT_OPTIONS = {
209
+ configFile: DEFAULT_CONFIG_FILE,
210
+ isRequired: false,
211
+ encoding: DEFAULT_ENCODING,
212
+ pathResolution: undefined
213
+ };
214
+ /**
215
+ * Default features enabled when creating a Cardigantime instance.
216
+ * Currently includes only the 'config' feature for configuration file support.
217
+ */ const DEFAULT_FEATURES = [
218
+ 'config'
219
+ ];
220
+ /**
221
+ * Default logger implementation using console methods.
222
+ * Provides basic logging functionality when no custom logger is specified.
223
+ * The verbose and silly methods are no-ops to avoid excessive output.
224
+ */ const DEFAULT_LOGGER = {
225
+ // eslint-disable-next-line no-console
226
+ debug: console.debug,
227
+ // eslint-disable-next-line no-console
228
+ info: console.info,
229
+ // eslint-disable-next-line no-console
230
+ warn: console.warn,
231
+ // eslint-disable-next-line no-console
232
+ error: console.error,
233
+ verbose: ()=>{},
234
+ silly: ()=>{}
235
+ };
236
+
237
+ function _define_property$1(obj, key, value) {
238
+ if (key in obj) {
239
+ Object.defineProperty(obj, key, {
240
+ value: value,
241
+ enumerable: true,
242
+ configurable: true,
243
+ writable: true
244
+ });
245
+ } else {
246
+ obj[key] = value;
247
+ }
248
+ return obj;
249
+ }
250
+ /**
251
+ * Error thrown when file system operations fail
252
+ */ class FileSystemError extends Error {
253
+ /**
254
+ * Creates an error for when a required directory doesn't exist
255
+ */ static directoryNotFound(path, isRequired = false) {
256
+ const message = isRequired ? 'Configuration directory does not exist and is required' : 'Configuration directory not found';
257
+ return new FileSystemError('not_found', message, path, 'directory_access');
258
+ }
259
+ /**
260
+ * Creates an error for when a directory exists but isn't readable
261
+ */ static directoryNotReadable(path) {
262
+ const message = 'Configuration directory exists but is not readable';
263
+ return new FileSystemError('not_readable', message, path, 'directory_read');
264
+ }
265
+ /**
266
+ * Creates an error for directory creation failures
267
+ */ static directoryCreationFailed(path, originalError) {
268
+ const message = 'Failed to create directory: ' + (originalError.message || 'Unknown error');
269
+ return new FileSystemError('creation_failed', message, path, 'directory_create', originalError);
270
+ }
271
+ /**
272
+ * Creates an error for file operation failures (glob, etc.)
273
+ */ static operationFailed(operation, path, originalError) {
274
+ const message = `Failed to ${operation}: ${originalError.message || 'Unknown error'}`;
275
+ return new FileSystemError('operation_failed', message, path, operation, originalError);
276
+ }
277
+ /**
278
+ * Creates an error for when a file is not found
279
+ */ static fileNotFound(path) {
280
+ const message = 'Configuration file not found';
281
+ return new FileSystemError('not_found', message, path, 'file_read');
282
+ }
283
+ constructor(errorType, message, path, operation, originalError){
284
+ super(message), _define_property$1(this, "errorType", void 0), _define_property$1(this, "path", void 0), _define_property$1(this, "operation", void 0), _define_property$1(this, "originalError", void 0);
285
+ this.name = 'FileSystemError';
286
+ this.errorType = errorType;
287
+ this.path = path;
288
+ this.operation = operation;
289
+ this.originalError = originalError;
290
+ }
291
+ }
292
+
293
+ const create$1 = (params)=>{
294
+ // eslint-disable-next-line no-console
295
+ const log = params.log || console.log;
296
+ const exists = async (path)=>{
297
+ try {
298
+ await fs__namespace.promises.stat(path);
299
+ return true;
300
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
301
+ } catch (error) {
302
+ return false;
303
+ }
304
+ };
305
+ const isDirectory = async (path)=>{
306
+ const stats = await fs__namespace.promises.stat(path);
307
+ if (!stats.isDirectory()) {
308
+ log(`${path} is not a directory`);
309
+ return false;
310
+ }
311
+ return true;
312
+ };
313
+ const isFile = async (path)=>{
314
+ const stats = await fs__namespace.promises.stat(path);
315
+ if (!stats.isFile()) {
316
+ log(`${path} is not a file`);
317
+ return false;
318
+ }
319
+ return true;
320
+ };
321
+ const isReadable = async (path)=>{
322
+ try {
323
+ await fs__namespace.promises.access(path, fs__namespace.constants.R_OK);
324
+ } catch (error) {
325
+ log(`${path} is not readable: %s %s`, error.message, error.stack);
326
+ return false;
327
+ }
328
+ return true;
329
+ };
330
+ const isWritable = async (path)=>{
331
+ try {
332
+ await fs__namespace.promises.access(path, fs__namespace.constants.W_OK);
333
+ } catch (error) {
334
+ log(`${path} is not writable: %s %s`, error.message, error.stack);
335
+ return false;
336
+ }
337
+ return true;
338
+ };
339
+ const isFileReadable = async (path)=>{
340
+ return await exists(path) && await isFile(path) && await isReadable(path);
341
+ };
342
+ const isDirectoryWritable = async (path)=>{
343
+ return await exists(path) && await isDirectory(path) && await isWritable(path);
344
+ };
345
+ const isDirectoryReadable = async (path)=>{
346
+ return await exists(path) && await isDirectory(path) && await isReadable(path);
347
+ };
348
+ const createDirectory = async (path)=>{
349
+ try {
350
+ await fs__namespace.promises.mkdir(path, {
351
+ recursive: true
352
+ });
353
+ } catch (mkdirError) {
354
+ throw FileSystemError.directoryCreationFailed(path, mkdirError);
355
+ }
356
+ };
357
+ const readFile = async (path, encoding)=>{
358
+ // Validate encoding parameter
359
+ const validEncodings = [
360
+ 'utf8',
361
+ 'utf-8',
362
+ 'ascii',
363
+ 'latin1',
364
+ 'base64',
365
+ 'hex',
366
+ 'utf16le',
367
+ 'ucs2',
368
+ 'ucs-2'
369
+ ];
370
+ if (!validEncodings.includes(encoding.toLowerCase())) {
371
+ throw new Error('Invalid encoding specified');
372
+ }
373
+ // Check file size before reading to prevent DoS
374
+ try {
375
+ const stats = await fs__namespace.promises.stat(path);
376
+ const maxFileSize = 10 * 1024 * 1024; // 10MB limit
377
+ if (stats.size > maxFileSize) {
378
+ throw new Error('File too large to process');
379
+ }
380
+ } catch (error) {
381
+ if (error.code === 'ENOENT') {
382
+ throw FileSystemError.fileNotFound(path);
383
+ }
384
+ throw error;
385
+ }
386
+ return await fs__namespace.promises.readFile(path, {
387
+ encoding: encoding
388
+ });
389
+ };
390
+ const writeFile = async (path, data, encoding)=>{
391
+ await fs__namespace.promises.writeFile(path, data, {
392
+ encoding: encoding
393
+ });
394
+ };
395
+ const forEachFileIn = async (directory, callback, options = {
396
+ pattern: '*.*'
397
+ })=>{
398
+ try {
399
+ const files = await glob.glob(options.pattern, {
400
+ cwd: directory,
401
+ nodir: true
402
+ });
403
+ for (const file of files){
404
+ await callback(path__namespace.join(directory, file));
405
+ }
406
+ } catch (err) {
407
+ throw FileSystemError.operationFailed(`glob pattern ${options.pattern}`, directory, err);
408
+ }
409
+ };
410
+ const readStream = async (path)=>{
411
+ return fs__namespace.createReadStream(path);
412
+ };
413
+ const hashFile = async (path, length)=>{
414
+ const file = await readFile(path, 'utf8');
415
+ return crypto__namespace.createHash('sha256').update(file).digest('hex').slice(0, length);
416
+ };
417
+ const listFiles = async (directory)=>{
418
+ return await fs__namespace.promises.readdir(directory);
419
+ };
420
+ return {
421
+ exists,
422
+ isDirectory,
423
+ isFile,
424
+ isReadable,
425
+ isWritable,
426
+ isFileReadable,
427
+ isDirectoryWritable,
428
+ isDirectoryReadable,
429
+ createDirectory,
430
+ readFile,
431
+ readStream,
432
+ writeFile,
433
+ forEachFileIn,
434
+ hashFile,
435
+ listFiles
436
+ };
437
+ };
438
+
439
+ /**
440
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
441
+ */ function resolveConfigPaths$1(config, configDir, pathFields = [], resolvePathArray = []) {
442
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
443
+ return config;
444
+ }
445
+ const resolvedConfig = {
446
+ ...config
447
+ };
448
+ for (const fieldPath of pathFields){
449
+ const value = getNestedValue$1(resolvedConfig, fieldPath);
450
+ if (value !== undefined) {
451
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
452
+ const resolvedValue = resolvePathValue$1(value, configDir, shouldResolveArrayElements);
453
+ setNestedValue$1(resolvedConfig, fieldPath, resolvedValue);
454
+ }
455
+ }
456
+ return resolvedConfig;
457
+ }
458
+ /**
459
+ * Gets a nested value from an object using dot notation.
460
+ */ function getNestedValue$1(obj, path) {
461
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
462
+ }
463
+ /**
464
+ * Sets a nested value in an object using dot notation.
465
+ */ function isUnsafeKey$1(key) {
466
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
467
+ }
468
+ function setNestedValue$1(obj, path, value) {
469
+ const keys = path.split('.');
470
+ const lastKey = keys.pop();
471
+ // Prevent prototype pollution via special property names
472
+ if (isUnsafeKey$1(lastKey) || keys.some(isUnsafeKey$1)) {
473
+ return;
474
+ }
475
+ const target = keys.reduce((current, key)=>{
476
+ // Skip if this is an unsafe key (already checked above, but defensive)
477
+ if (isUnsafeKey$1(key)) {
478
+ return current;
479
+ }
480
+ if (!(key in current)) {
481
+ current[key] = {};
482
+ }
483
+ return current[key];
484
+ }, obj);
485
+ target[lastKey] = value;
486
+ }
487
+ /**
488
+ * Resolves a path value (string or array of strings) relative to the config directory.
489
+ */ function resolvePathValue$1(value, configDir, resolveArrayElements) {
490
+ if (typeof value === 'string') {
491
+ return resolveSinglePath$1(value, configDir);
492
+ }
493
+ if (Array.isArray(value) && resolveArrayElements) {
494
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath$1(item, configDir) : item);
495
+ }
496
+ return value;
497
+ }
498
+ /**
499
+ * Resolves a single path string relative to the config directory if it's a relative path.
500
+ */ function resolveSinglePath$1(pathStr, configDir) {
501
+ if (!pathStr || path__namespace.isAbsolute(pathStr)) {
502
+ return pathStr;
503
+ }
504
+ return path__namespace.resolve(configDir, pathStr);
505
+ }
506
+ /**
507
+ * Discovers configuration directories by traversing up the directory tree.
508
+ *
509
+ * Starting from the specified directory (or current working directory),
510
+ * this function searches for directories with the given name, continuing
511
+ * up the directory tree until it reaches the filesystem root or the
512
+ * maximum number of levels.
513
+ *
514
+ * @param options Configuration options for discovery
515
+ * @returns Promise resolving to array of discovered configuration directories
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * const dirs = await discoverConfigDirectories({
520
+ * configDirName: '.kodrdriv',
521
+ * configFileName: 'config.yaml',
522
+ * maxLevels: 5
523
+ * });
524
+ * // Returns: [
525
+ * // { path: '/project/.kodrdriv', level: 0 },
526
+ * // { path: '/project/parent/.kodrdriv', level: 1 }
527
+ * // ]
528
+ * ```
529
+ */ async function discoverConfigDirectories(options) {
530
+ const { configDirName, maxLevels = 10, startingDir = process.cwd(), logger } = options;
531
+ const storage = create$1({
532
+ log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
533
+ });
534
+ const discoveredDirs = [];
535
+ let currentDir = path__namespace.resolve(startingDir);
536
+ let level = 0;
537
+ const visited = new Set(); // Prevent infinite loops with symlinks
538
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Starting hierarchical discovery from: ${currentDir}`);
539
+ while(level < maxLevels){
540
+ // Prevent infinite loops with symlinks
541
+ const realPath = path__namespace.resolve(currentDir);
542
+ if (visited.has(realPath)) {
543
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Already visited ${realPath}, stopping discovery`);
544
+ break;
545
+ }
546
+ visited.add(realPath);
547
+ const configDirPath = path__namespace.join(currentDir, configDirName);
548
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Checking for config directory: ${configDirPath}`);
549
+ try {
550
+ const exists = await storage.exists(configDirPath);
551
+ const isReadable = exists && await storage.isDirectoryReadable(configDirPath);
552
+ if (exists && isReadable) {
553
+ discoveredDirs.push({
554
+ path: configDirPath,
555
+ level
556
+ });
557
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Found config directory at level ${level}: ${configDirPath}`);
558
+ } else if (exists && !isReadable) {
559
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config directory exists but is not readable: ${configDirPath}`);
560
+ }
561
+ } catch (error) {
562
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Error checking config directory ${configDirPath}: ${error.message}`);
563
+ }
564
+ // Move up one directory level
565
+ const parentDir = path__namespace.dirname(currentDir);
566
+ // Check if we've reached the root directory
567
+ if (parentDir === currentDir) {
568
+ logger === null || logger === void 0 ? void 0 : logger.debug('Reached filesystem root, stopping discovery');
569
+ break;
570
+ }
571
+ currentDir = parentDir;
572
+ level++;
573
+ }
574
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Discovery complete. Found ${discoveredDirs.length} config directories`);
575
+ return discoveredDirs;
576
+ }
577
+ /**
578
+ * Tries to find a config file with alternative extensions (.yaml or .yml).
579
+ *
580
+ * @param storage Storage instance to use for file operations
581
+ * @param configDir The directory containing the config file
582
+ * @param configFileName The base config file name (may have .yaml or .yml extension)
583
+ * @param logger Optional logger for debugging
584
+ * @returns Promise resolving to the found config file path or null if not found
585
+ */ async function findConfigFileWithExtension$1(storage, configDir, configFileName, logger) {
586
+ const configFilePath = path__namespace.join(configDir, configFileName);
587
+ // First try the exact filename as specified
588
+ const exists = await storage.exists(configFilePath);
589
+ if (exists) {
590
+ const isReadable = await storage.isFileReadable(configFilePath);
591
+ if (isReadable) {
592
+ return configFilePath;
593
+ }
594
+ }
595
+ // If the exact filename doesn't exist or isn't readable, try alternative extensions
596
+ // Only do this if the filename has a .yaml or .yml extension
597
+ const ext = path__namespace.extname(configFileName);
598
+ if (ext === '.yaml' || ext === '.yml') {
599
+ const baseName = path__namespace.basename(configFileName, ext);
600
+ const alternativeExt = ext === '.yaml' ? '.yml' : '.yaml';
601
+ const alternativePath = path__namespace.join(configDir, baseName + alternativeExt);
602
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config file not found at ${configFilePath}, trying alternative: ${alternativePath}`);
603
+ const altExists = await storage.exists(alternativePath);
604
+ if (altExists) {
605
+ const altIsReadable = await storage.isFileReadable(alternativePath);
606
+ if (altIsReadable) {
607
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Found config file with alternative extension: ${alternativePath}`);
608
+ return alternativePath;
609
+ }
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+ /**
615
+ * Loads and parses a configuration file from a directory.
616
+ *
617
+ * @param configDir Path to the configuration directory
618
+ * @param configFileName Name of the configuration file
619
+ * @param encoding File encoding
620
+ * @param logger Optional logger
621
+ * @param pathFields Optional array of field names that contain paths to be resolved
622
+ * @param resolvePathArray Optional array of field names whose array elements should all be resolved as paths
623
+ * @returns Promise resolving to parsed configuration object or null if not found
624
+ */ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray) {
625
+ const storage = create$1({
626
+ log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
627
+ });
628
+ try {
629
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Attempting to load config file: ${path__namespace.join(configDir, configFileName)}`);
630
+ // Try to find the config file with alternative extensions
631
+ const configFilePath = await findConfigFileWithExtension$1(storage, configDir, configFileName, logger);
632
+ if (!configFilePath) {
633
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config file does not exist: ${path__namespace.join(configDir, configFileName)}`);
634
+ return null;
635
+ }
636
+ const yamlContent = await storage.readFile(configFilePath, encoding);
637
+ const parsedYaml = yaml__namespace.load(yamlContent);
638
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
639
+ let config = parsedYaml;
640
+ // Apply path resolution if configured
641
+ if (pathFields && pathFields.length > 0) {
642
+ config = resolveConfigPaths$1(config, configDir, pathFields, resolvePathArray || []);
643
+ }
644
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Successfully loaded config from: ${configFilePath}`);
645
+ return config;
646
+ } else {
647
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config file contains invalid format: ${configFilePath}`);
648
+ return null;
649
+ }
650
+ } catch (error) {
651
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Error loading config from ${path__namespace.join(configDir, configFileName)}: ${error.message}`);
652
+ return null;
653
+ }
654
+ }
655
+ /**
656
+ * Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
657
+ *
658
+ * Objects are merged from lowest precedence to highest precedence,
659
+ * meaning that properties in later objects override properties in earlier objects.
660
+ * Arrays can be merged using different strategies based on the fieldOverlaps configuration.
661
+ *
662
+ * @param configs Array of configuration objects, ordered from lowest to highest precedence
663
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
664
+ * @returns Merged configuration object
665
+ *
666
+ * @example
667
+ * ```typescript
668
+ * const merged = deepMergeConfigs([
669
+ * { api: { timeout: 5000 }, features: ['auth'] }, // Lower precedence
670
+ * { api: { retries: 3 }, features: ['analytics'] }, // Higher precedence
671
+ * ], {
672
+ * 'features': 'append' // Results in features: ['auth', 'analytics']
673
+ * });
674
+ * ```
675
+ */ function deepMergeConfigs(configs, fieldOverlaps) {
676
+ if (configs.length === 0) {
677
+ return {};
678
+ }
679
+ if (configs.length === 1) {
680
+ return {
681
+ ...configs[0]
682
+ };
683
+ }
684
+ return configs.reduce((merged, current)=>{
685
+ return deepMergeTwo(merged, current, fieldOverlaps);
686
+ }, {});
687
+ }
688
+ /**
689
+ * Deep merges two objects with proper precedence and configurable array overlap behavior.
690
+ *
691
+ * @param target Target object (lower precedence)
692
+ * @param source Source object (higher precedence)
693
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
694
+ * @param currentPath Current field path for nested merging (used internally)
695
+ * @returns Merged object
696
+ */ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
697
+ // Handle null/undefined
698
+ if (source == null) return target;
699
+ if (target == null) return source;
700
+ // Handle non-objects (primitives, arrays, functions, etc.)
701
+ if (typeof source !== 'object' || typeof target !== 'object') {
702
+ return source; // Source takes precedence
703
+ }
704
+ // Handle arrays with configurable overlap behavior
705
+ if (Array.isArray(source)) {
706
+ if (Array.isArray(target) && fieldOverlaps) {
707
+ const overlapMode = getOverlapModeForPath(currentPath, fieldOverlaps);
708
+ return mergeArrays(target, source, overlapMode);
709
+ } else {
710
+ // Default behavior: replace entirely
711
+ return [
712
+ ...source
713
+ ];
714
+ }
715
+ }
716
+ if (Array.isArray(target)) {
717
+ return source; // Source object replaces target array
718
+ }
719
+ // Deep merge objects
720
+ const result = {
721
+ ...target
722
+ };
723
+ for(const key in source){
724
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
725
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
726
+ if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
727
+ // Recursively merge nested objects
728
+ result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
729
+ } else {
730
+ // Handle arrays and primitives with overlap consideration
731
+ if (Array.isArray(source[key]) && Array.isArray(result[key]) && fieldOverlaps) {
732
+ const overlapMode = getOverlapModeForPath(fieldPath, fieldOverlaps);
733
+ result[key] = mergeArrays(result[key], source[key], overlapMode);
734
+ } else {
735
+ // Replace with source value (higher precedence)
736
+ result[key] = source[key];
737
+ }
738
+ }
739
+ }
740
+ }
741
+ return result;
742
+ }
743
+ /**
744
+ * Determines the overlap mode for a given field path.
745
+ *
746
+ * @param fieldPath The current field path (dot notation)
747
+ * @param fieldOverlaps Configuration mapping field paths to overlap modes
748
+ * @returns The overlap mode to use for this field path
749
+ */ function getOverlapModeForPath(fieldPath, fieldOverlaps) {
750
+ // Check for exact match first
751
+ if (fieldPath in fieldOverlaps) {
752
+ return fieldOverlaps[fieldPath];
753
+ }
754
+ // Check for any parent path matches (for nested configurations)
755
+ const pathParts = fieldPath.split('.');
756
+ for(let i = pathParts.length - 1; i > 0; i--){
757
+ const parentPath = pathParts.slice(0, i).join('.');
758
+ if (parentPath in fieldOverlaps) {
759
+ return fieldOverlaps[parentPath];
760
+ }
761
+ }
762
+ // Default to override if no specific configuration found
763
+ return 'override';
764
+ }
765
+ /**
766
+ * Merges two arrays based on the specified overlap mode.
767
+ *
768
+ * @param targetArray The lower precedence array
769
+ * @param sourceArray The higher precedence array
770
+ * @param mode The overlap mode to use
771
+ * @returns The merged array
772
+ */ function mergeArrays(targetArray, sourceArray, mode) {
773
+ switch(mode){
774
+ case 'append':
775
+ return [
776
+ ...targetArray,
777
+ ...sourceArray
778
+ ];
779
+ case 'prepend':
780
+ return [
781
+ ...sourceArray,
782
+ ...targetArray
783
+ ];
784
+ case 'override':
785
+ default:
786
+ return [
787
+ ...sourceArray
788
+ ];
789
+ }
790
+ }
791
+ /**
792
+ * Loads configurations from multiple directories and merges them with proper precedence.
793
+ *
794
+ * This is the main function for hierarchical configuration loading. It:
795
+ * 1. Discovers configuration directories up the directory tree
796
+ * 2. Loads configuration files from each discovered directory
797
+ * 3. Merges them with proper precedence (closer directories win)
798
+ * 4. Returns the merged configuration with metadata
799
+ *
800
+ * @param options Configuration options for hierarchical loading
801
+ * @returns Promise resolving to hierarchical configuration result
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * const result = await loadHierarchicalConfig({
806
+ * configDirName: '.kodrdriv',
807
+ * configFileName: 'config.yaml',
808
+ * startingDir: '/project/subdir',
809
+ * maxLevels: 5,
810
+ * fieldOverlaps: {
811
+ * 'features': 'append',
812
+ * 'excludePatterns': 'prepend'
813
+ * }
814
+ * });
815
+ *
816
+ * // result.config contains merged configuration with custom array merging
817
+ * // result.discoveredDirs shows where configs were found
818
+ * // result.errors contains any non-fatal errors
819
+ * ```
820
+ */ async function loadHierarchicalConfig(options) {
821
+ const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
822
+ logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
823
+ // Discover all configuration directories
824
+ const discoveredDirs = await discoverConfigDirectories(options);
825
+ if (discoveredDirs.length === 0) {
826
+ logger === null || logger === void 0 ? void 0 : logger.verbose('No configuration directories found');
827
+ return {
828
+ config: {},
829
+ discoveredDirs: [],
830
+ resolvedConfigDirs: [],
831
+ errors: []
832
+ };
833
+ }
834
+ // Load configurations from each directory
835
+ const configs = [];
836
+ const resolvedConfigDirs = [];
837
+ const errors = [];
838
+ // Sort by level (highest level first = lowest precedence first)
839
+ const sortedDirs = [
840
+ ...discoveredDirs
841
+ ].sort((a, b)=>b.level - a.level);
842
+ for (const dir of sortedDirs){
843
+ try {
844
+ const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger, pathFields, resolvePathArray);
845
+ if (config !== null) {
846
+ configs.push(config);
847
+ resolvedConfigDirs.push(dir);
848
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
849
+ } else {
850
+ logger === null || logger === void 0 ? void 0 : logger.debug(`No valid config found at level ${dir.level}: ${dir.path}`);
851
+ }
852
+ } catch (error) {
853
+ const errorMsg = `Failed to load config from ${dir.path}: ${error.message}`;
854
+ errors.push(errorMsg);
855
+ logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
856
+ }
857
+ }
858
+ // Merge all configurations with proper precedence and configurable array overlap
859
+ const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
860
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
861
+ return {
862
+ config: mergedConfig,
863
+ discoveredDirs,
864
+ resolvedConfigDirs,
865
+ errors
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Removes undefined values from an object to create a clean configuration.
871
+ * This is used to merge configuration sources while avoiding undefined pollution.
872
+ *
873
+ * @param obj - The object to clean
874
+ * @returns A new object with undefined values filtered out
875
+ */ function clean(obj) {
876
+ return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
877
+ }
878
+ /**
879
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
880
+ *
881
+ * @param config - The configuration object to process
882
+ * @param configDir - The directory containing the configuration file
883
+ * @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
884
+ * @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
885
+ * @returns The configuration object with resolved paths
886
+ */ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
887
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
888
+ return config;
889
+ }
890
+ const resolvedConfig = {
891
+ ...config
892
+ };
893
+ for (const fieldPath of pathFields){
894
+ const value = getNestedValue(resolvedConfig, fieldPath);
895
+ if (value !== undefined) {
896
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
897
+ const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
898
+ setNestedValue(resolvedConfig, fieldPath, resolvedValue);
899
+ }
900
+ }
901
+ return resolvedConfig;
902
+ }
903
+ /**
904
+ * Gets a nested value from an object using dot notation.
905
+ */ function getNestedValue(obj, path) {
906
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
907
+ }
908
+ /**
909
+ * Checks if a key is unsafe for prototype pollution prevention.
910
+ */ function isUnsafeKey(key) {
911
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
912
+ }
913
+ /**
914
+ * Sets a nested value in an object using dot notation.
915
+ * Prevents prototype pollution by rejecting dangerous property names.
916
+ */ function setNestedValue(obj, path, value) {
917
+ const keys = path.split('.');
918
+ const lastKey = keys.pop();
919
+ // Prevent prototype pollution via special property names
920
+ if (isUnsafeKey(lastKey) || keys.some(isUnsafeKey)) {
921
+ return;
922
+ }
923
+ const target = keys.reduce((current, key)=>{
924
+ // Skip if this is an unsafe key (already checked above, but defensive)
925
+ if (isUnsafeKey(key)) {
926
+ return current;
927
+ }
928
+ if (!(key in current)) {
929
+ current[key] = {};
930
+ }
931
+ return current[key];
932
+ }, obj);
933
+ target[lastKey] = value;
934
+ }
935
+ /**
936
+ * Resolves a path value (string or array of strings) relative to the config directory.
937
+ */ function resolvePathValue(value, configDir, resolveArrayElements) {
938
+ if (typeof value === 'string') {
939
+ return resolveSinglePath(value, configDir);
940
+ }
941
+ if (Array.isArray(value) && resolveArrayElements) {
942
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
943
+ }
944
+ return value;
945
+ }
946
+ /**
947
+ * Resolves a single path string relative to the config directory if it's a relative path.
948
+ */ function resolveSinglePath(pathStr, configDir) {
949
+ if (!pathStr || path__namespace.isAbsolute(pathStr)) {
950
+ return pathStr;
951
+ }
952
+ return path__namespace.resolve(configDir, pathStr);
953
+ }
954
+ /**
955
+ * Validates and secures a user-provided path to prevent path traversal attacks.
956
+ *
957
+ * Security checks include:
958
+ * - Path traversal prevention (blocks '..')
959
+ * - Absolute path detection
960
+ * - Path separator validation
961
+ *
962
+ * @param userPath - The user-provided path component
963
+ * @param basePath - The base directory to join the path with
964
+ * @returns The safely joined and normalized path
965
+ * @throws {Error} When path traversal or absolute paths are detected
966
+ */ function validatePath(userPath, basePath) {
967
+ if (!userPath || !basePath) {
968
+ throw new Error('Invalid path parameters');
969
+ }
970
+ const normalized = path__namespace.normalize(userPath);
971
+ // Prevent path traversal attacks
972
+ if (normalized.includes('..') || path__namespace.isAbsolute(normalized)) {
973
+ throw new Error('Invalid path: path traversal detected');
974
+ }
975
+ // Ensure the path doesn't start with a path separator
976
+ if (normalized.startsWith('/') || normalized.startsWith('\\')) {
977
+ throw new Error('Invalid path: absolute path detected');
978
+ }
979
+ return path__namespace.join(basePath, normalized);
980
+ }
981
+ /**
982
+ * Validates a configuration directory path for security and basic formatting.
983
+ *
984
+ * Performs validation to prevent:
985
+ * - Null byte injection attacks
986
+ * - Extremely long paths that could cause DoS
987
+ * - Empty or invalid directory specifications
988
+ *
989
+ * @param configDir - The configuration directory path to validate
990
+ * @returns The normalized configuration directory path
991
+ * @throws {Error} When the directory path is invalid or potentially dangerous
992
+ */ function validateConfigDirectory$1(configDir) {
993
+ if (!configDir) {
994
+ throw new Error('Configuration directory is required');
995
+ }
996
+ // Check for null bytes which could be used for path injection
997
+ if (configDir.includes('\0')) {
998
+ throw new Error('Invalid path: null byte detected');
999
+ }
1000
+ const normalized = path__namespace.normalize(configDir);
1001
+ // Basic validation - could be expanded based on requirements
1002
+ if (normalized.length > 1000) {
1003
+ throw new Error('Configuration directory path too long');
1004
+ }
1005
+ return normalized;
1006
+ }
1007
+ /**
1008
+ * Reads configuration from files and merges it with CLI arguments.
1009
+ *
1010
+ * This function implements the core configuration loading logic:
1011
+ * 1. Validates and resolves the configuration directory path
1012
+ * 2. Attempts to read the YAML configuration file
1013
+ * 3. Safely parses the YAML content with security protections
1014
+ * 4. Merges file configuration with runtime arguments
1015
+ * 5. Returns a typed configuration object
1016
+ *
1017
+ * The function handles missing files gracefully and provides detailed
1018
+ * logging for troubleshooting configuration issues.
1019
+ *
1020
+ * @template T - The Zod schema shape type for configuration validation
1021
+ * @param args - Parsed command-line arguments containing potential config overrides
1022
+ * @param options - Cardigantime options with defaults, schema, and logger
1023
+ * @returns Promise resolving to the merged and typed configuration object
1024
+ * @throws {Error} When configuration directory is invalid or required files cannot be read
1025
+ *
1026
+ * @example
1027
+ * ```typescript
1028
+ * const config = await read(cliArgs, {
1029
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
1030
+ * configShape: MySchema.shape,
1031
+ * logger: console,
1032
+ * features: ['config']
1033
+ * });
1034
+ * // config is fully typed based on your schema
1035
+ * ```
1036
+ */ const read = async (args, options)=>{
1037
+ var _options_defaults, _options_defaults_pathResolution;
1038
+ const logger = options.logger;
1039
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
1040
+ if (!rawConfigDir) {
1041
+ throw new Error('Configuration directory must be specified');
1042
+ }
1043
+ const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
1044
+ logger.verbose('Resolved config directory');
1045
+ let rawFileConfig = {};
1046
+ let discoveredConfigDirs = [];
1047
+ let resolvedConfigDirs = [];
1048
+ // Check if hierarchical configuration discovery is enabled
1049
+ // Use optional chaining for safety although options.features is defaulted
1050
+ if (options.features && options.features.includes('hierarchical')) {
1051
+ logger.verbose('Hierarchical configuration discovery enabled');
1052
+ try {
1053
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
1054
+ // Extract the config directory name from the path for hierarchical discovery
1055
+ const configDirName = path__namespace.basename(resolvedConfigDir);
1056
+ const startingDir = path__namespace.dirname(resolvedConfigDir);
1057
+ logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
1058
+ const hierarchicalResult = await loadHierarchicalConfig({
1059
+ configDirName,
1060
+ configFileName: options.defaults.configFile,
1061
+ startingDir,
1062
+ encoding: options.defaults.encoding,
1063
+ logger,
1064
+ pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
1065
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
1066
+ fieldOverlaps: options.defaults.fieldOverlaps
1067
+ });
1068
+ rawFileConfig = hierarchicalResult.config;
1069
+ discoveredConfigDirs = hierarchicalResult.discoveredDirs.map((dir)=>dir.path);
1070
+ resolvedConfigDirs = hierarchicalResult.resolvedConfigDirs.map((dir)=>dir.path);
1071
+ if (hierarchicalResult.discoveredDirs.length > 0) {
1072
+ logger.verbose(`Hierarchical discovery found ${hierarchicalResult.discoveredDirs.length} configuration directories`);
1073
+ hierarchicalResult.discoveredDirs.forEach((dir)=>{
1074
+ logger.debug(` Level ${dir.level}: ${dir.path}`);
1075
+ });
1076
+ } else {
1077
+ logger.verbose('No configuration directories found in hierarchy');
1078
+ }
1079
+ if (hierarchicalResult.resolvedConfigDirs.length > 0) {
1080
+ logger.verbose(`Found ${hierarchicalResult.resolvedConfigDirs.length} directories with actual configuration files`);
1081
+ hierarchicalResult.resolvedConfigDirs.forEach((dir)=>{
1082
+ logger.debug(` Config dir level ${dir.level}: ${dir.path}`);
1083
+ });
1084
+ }
1085
+ if (hierarchicalResult.errors.length > 0) {
1086
+ hierarchicalResult.errors.forEach((error)=>logger.warn(`Hierarchical config warning: ${error}`));
1087
+ }
1088
+ } catch (error) {
1089
+ logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
1090
+ // Fall back to single directory mode
1091
+ logger.verbose('Falling back to single directory configuration loading');
1092
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
1093
+ // Include the directory in both arrays (discovered but check if it had config)
1094
+ discoveredConfigDirs = [
1095
+ resolvedConfigDir
1096
+ ];
1097
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
1098
+ resolvedConfigDirs = [
1099
+ resolvedConfigDir
1100
+ ];
1101
+ } else {
1102
+ resolvedConfigDirs = [];
1103
+ }
1104
+ }
1105
+ } else {
1106
+ // Use traditional single directory configuration loading
1107
+ logger.verbose('Using single directory configuration loading');
1108
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
1109
+ // Include the directory in discovered, and in resolved only if it had config
1110
+ discoveredConfigDirs = [
1111
+ resolvedConfigDir
1112
+ ];
1113
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
1114
+ resolvedConfigDirs = [
1115
+ resolvedConfigDir
1116
+ ];
1117
+ } else {
1118
+ resolvedConfigDirs = [];
1119
+ }
1120
+ }
1121
+ // Apply path resolution if configured
1122
+ let processedConfig = rawFileConfig;
1123
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
1124
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
1125
+ }
1126
+ const config = clean({
1127
+ ...processedConfig,
1128
+ ...{
1129
+ configDirectory: resolvedConfigDir,
1130
+ discoveredConfigDirs,
1131
+ resolvedConfigDirs
1132
+ }
1133
+ });
1134
+ return config;
1135
+ };
1136
+ /**
1137
+ * Tries to find a config file with alternative extensions (.yaml or .yml).
1138
+ *
1139
+ * @param storage Storage instance to use for file operations
1140
+ * @param configDir The directory containing the config file
1141
+ * @param configFileName The base config file name (may have .yaml or .yml extension)
1142
+ * @param logger Logger for debugging
1143
+ * @returns Promise resolving to the found config file path or null if not found
1144
+ */ async function findConfigFileWithExtension(storage, configDir, configFileName, logger) {
1145
+ // Validate the config file name to prevent path traversal
1146
+ const configFilePath = validatePath(configFileName, configDir);
1147
+ // First try the exact filename as specified
1148
+ const exists = await storage.exists(configFilePath);
1149
+ if (exists) {
1150
+ const isReadable = await storage.isFileReadable(configFilePath);
1151
+ if (isReadable) {
1152
+ return configFilePath;
1153
+ }
1154
+ }
1155
+ // If the exact filename doesn't exist or isn't readable, try alternative extensions
1156
+ // Only do this if the filename has a .yaml or .yml extension
1157
+ const ext = path__namespace.extname(configFileName);
1158
+ if (ext === '.yaml' || ext === '.yml') {
1159
+ const baseName = path__namespace.basename(configFileName, ext);
1160
+ const alternativeExt = ext === '.yaml' ? '.yml' : '.yaml';
1161
+ const alternativeFileName = baseName + alternativeExt;
1162
+ const alternativePath = validatePath(alternativeFileName, configDir);
1163
+ logger.debug(`Config file not found at ${configFilePath}, trying alternative: ${alternativePath}`);
1164
+ const altExists = await storage.exists(alternativePath);
1165
+ if (altExists) {
1166
+ const altIsReadable = await storage.isFileReadable(alternativePath);
1167
+ if (altIsReadable) {
1168
+ logger.debug(`Found config file with alternative extension: ${alternativePath}`);
1169
+ return alternativePath;
1170
+ }
1171
+ }
1172
+ }
1173
+ return null;
1174
+ }
1175
+ /**
1176
+ * Loads configuration from a single directory (traditional mode).
1177
+ *
1178
+ * @param resolvedConfigDir - The resolved configuration directory path
1179
+ * @param options - Cardigantime options
1180
+ * @param logger - Logger instance
1181
+ * @returns Promise resolving to the configuration object
1182
+ */ async function loadSingleDirectoryConfig(resolvedConfigDir, options, logger) {
1183
+ const storage = create$1({
1184
+ log: logger.debug
1185
+ });
1186
+ logger.verbose('Attempting to load config file for cardigantime');
1187
+ let rawFileConfig = {};
1188
+ try {
1189
+ // Try to find the config file with alternative extensions
1190
+ const configFilePath = await findConfigFileWithExtension(storage, resolvedConfigDir, options.defaults.configFile, logger);
1191
+ if (!configFilePath) {
1192
+ logger.verbose('Configuration file not found. Using empty configuration.');
1193
+ return rawFileConfig;
1194
+ }
1195
+ const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
1196
+ // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities
1197
+ const parsedYaml = yaml__namespace.load(yamlContent);
1198
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
1199
+ rawFileConfig = parsedYaml;
1200
+ logger.verbose('Loaded configuration file successfully');
1201
+ } else if (parsedYaml !== null) {
1202
+ logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
1203
+ }
1204
+ } catch (error) {
1205
+ // Re-throw security-related errors (path validation failures)
1206
+ if (error.message && /Invalid path|path traversal|absolute path/i.test(error.message)) {
1207
+ throw error;
1208
+ }
1209
+ if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
1210
+ logger.verbose('Configuration file not found. Using empty configuration.');
1211
+ } else {
1212
+ // SECURITY FIX: Don't expose internal paths or detailed error information
1213
+ logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
1214
+ }
1215
+ }
1216
+ return rawFileConfig;
1217
+ }
1218
+ /**
1219
+ * Recursively tracks the source of configuration values from hierarchical loading.
1220
+ *
1221
+ * @param config - The configuration object to track
1222
+ * @param sourcePath - Path to the configuration file
1223
+ * @param level - Hierarchical level
1224
+ * @param prefix - Current object path prefix for nested values
1225
+ * @param tracker - The tracker object to populate
1226
+ */ function trackConfigSources(config, sourcePath, level, prefix = '', tracker = {}) {
1227
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
1228
+ // For primitives and arrays, track the entire value
1229
+ tracker[prefix] = {
1230
+ value: config,
1231
+ sourcePath,
1232
+ level,
1233
+ sourceLabel: `Level ${level}: ${path__namespace.basename(path__namespace.dirname(sourcePath))}`
1234
+ };
1235
+ return tracker;
1236
+ }
1237
+ // For objects, recursively track each property
1238
+ for (const [key, value] of Object.entries(config)){
1239
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
1240
+ trackConfigSources(value, sourcePath, level, fieldPath, tracker);
1241
+ }
1242
+ return tracker;
1243
+ }
1244
+ /**
1245
+ * Merges multiple configuration source trackers with proper precedence.
1246
+ * Lower level numbers have higher precedence.
1247
+ *
1248
+ * @param trackers - Array of trackers from different config sources
1249
+ * @returns Merged tracker with proper precedence
1250
+ */ function mergeConfigTrackers(trackers) {
1251
+ const merged = {};
1252
+ for (const tracker of trackers){
1253
+ for (const [key, info] of Object.entries(tracker)){
1254
+ // Only update if we don't have this key yet, or if this source has higher precedence (lower level)
1255
+ if (!merged[key] || info.level < merged[key].level) {
1256
+ merged[key] = info;
1257
+ }
1258
+ }
1259
+ }
1260
+ return merged;
1261
+ }
1262
+ /**
1263
+ * Formats a configuration value for display, handling different types appropriately.
1264
+ *
1265
+ * @param value - The configuration value to format
1266
+ * @returns Formatted string representation
1267
+ */ function formatConfigValue(value) {
1268
+ if (value === null) return 'null';
1269
+ if (value === undefined) return 'undefined';
1270
+ if (typeof value === 'string') return `"${value}"`;
1271
+ if (typeof value === 'boolean') return value.toString();
1272
+ if (typeof value === 'number') return value.toString();
1273
+ if (Array.isArray(value)) {
1274
+ if (value.length === 0) return '[]';
1275
+ if (value.length <= 3) {
1276
+ return `[${value.map(formatConfigValue).join(', ')}]`;
1277
+ }
1278
+ return `[${value.slice(0, 2).map(formatConfigValue).join(', ')}, ... (${value.length} items)]`;
1279
+ }
1280
+ if (typeof value === 'object') {
1281
+ const keys = Object.keys(value);
1282
+ if (keys.length === 0) return '{}';
1283
+ if (keys.length <= 2) {
1284
+ return `{${keys.slice(0, 2).join(', ')}}`;
1285
+ }
1286
+ return `{${keys.slice(0, 2).join(', ')}, ... (${keys.length} keys)}`;
1287
+ }
1288
+ return String(value);
1289
+ }
1290
+ /**
1291
+ * Displays configuration with source tracking in a git blame-like format.
1292
+ *
1293
+ * @param config - The resolved configuration object
1294
+ * @param tracker - Configuration source tracker
1295
+ * @param discoveredDirs - Array of discovered configuration directories
1296
+ * @param logger - Logger instance for output
1297
+ */ function displayConfigWithSources(config, tracker, discoveredDirs, logger) {
1298
+ logger.info('\n' + '='.repeat(80));
1299
+ logger.info('CONFIGURATION SOURCE ANALYSIS');
1300
+ logger.info('='.repeat(80));
1301
+ // Display discovered configuration hierarchy
1302
+ logger.info('\nDISCOVERED CONFIGURATION HIERARCHY:');
1303
+ if (discoveredDirs.length === 0) {
1304
+ logger.info(' No configuration directories found in hierarchy');
1305
+ } else {
1306
+ discoveredDirs.sort((a, b)=>a.level - b.level) // Sort by precedence (lower level = higher precedence)
1307
+ .forEach((dir)=>{
1308
+ const precedence = dir.level === 0 ? '(highest precedence)' : dir.level === Math.max(...discoveredDirs.map((d)=>d.level)) ? '(lowest precedence)' : '';
1309
+ logger.info(` Level ${dir.level}: ${dir.path} ${precedence}`);
1310
+ });
1311
+ }
1312
+ // Display resolved configuration with sources
1313
+ logger.info('\nRESOLVED CONFIGURATION WITH SOURCES:');
1314
+ logger.info('Format: [Source] key: value\n');
1315
+ const sortedKeys = Object.keys(tracker).sort();
1316
+ const maxKeyLength = Math.max(...sortedKeys.map((k)=>k.length), 20);
1317
+ const maxSourceLength = Math.max(...Object.values(tracker).map((info)=>info.sourceLabel.length), 25);
1318
+ for (const key of sortedKeys){
1319
+ const info = tracker[key];
1320
+ const paddedKey = key.padEnd(maxKeyLength);
1321
+ const paddedSource = info.sourceLabel.padEnd(maxSourceLength);
1322
+ const formattedValue = formatConfigValue(info.value);
1323
+ logger.info(`[${paddedSource}] ${paddedKey}: ${formattedValue}`);
1324
+ }
1325
+ // Display summary
1326
+ logger.info('\n' + '-'.repeat(80));
1327
+ logger.info('SUMMARY:');
1328
+ logger.info(` Total configuration keys: ${Object.keys(tracker).length}`);
1329
+ logger.info(` Configuration sources: ${discoveredDirs.length}`);
1330
+ // Count values by source
1331
+ const sourceCount = {};
1332
+ for (const info of Object.values(tracker)){
1333
+ sourceCount[info.sourceLabel] = (sourceCount[info.sourceLabel] || 0) + 1;
1334
+ }
1335
+ logger.info(' Values by source:');
1336
+ for (const [source, count] of Object.entries(sourceCount)){
1337
+ logger.info(` ${source}: ${count} value(s)`);
1338
+ }
1339
+ logger.info('='.repeat(80));
1340
+ }
1341
+ /**
1342
+ * Checks and displays the resolved configuration with detailed source tracking.
1343
+ *
1344
+ * This function provides a git blame-like view of configuration resolution,
1345
+ * showing which file and hierarchical level contributed each configuration value.
1346
+ *
1347
+ * @template T - The Zod schema shape type for configuration validation
1348
+ * @param args - Parsed command-line arguments
1349
+ * @param options - Cardigantime options with defaults, schema, and logger
1350
+ * @returns Promise that resolves when the configuration check is complete
1351
+ *
1352
+ * @example
1353
+ * ```typescript
1354
+ * await checkConfig(cliArgs, {
1355
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
1356
+ * configShape: MySchema.shape,
1357
+ * logger: console,
1358
+ * features: ['config', 'hierarchical']
1359
+ * });
1360
+ * // Outputs detailed configuration source analysis
1361
+ * ```
1362
+ */ const checkConfig = async (args, options)=>{
1363
+ var _options_defaults, _options_defaults_pathResolution;
1364
+ const logger = options.logger;
1365
+ logger.info('Starting configuration check...');
1366
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
1367
+ if (!rawConfigDir) {
1368
+ throw new Error('Configuration directory must be specified');
1369
+ }
1370
+ const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
1371
+ logger.verbose(`Resolved config directory: ${resolvedConfigDir}`);
1372
+ let rawFileConfig = {};
1373
+ let discoveredDirs = [];
1374
+ let resolvedConfigDirs = [];
1375
+ let tracker = {};
1376
+ // Check if hierarchical configuration discovery is enabled
1377
+ // Use optional chaining for safety although options.features is defaulted
1378
+ if (options.features && options.features.includes('hierarchical')) {
1379
+ logger.verbose('Using hierarchical configuration discovery for source tracking');
1380
+ try {
1381
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
1382
+ // Extract the config directory name from the path for hierarchical discovery
1383
+ const configDirName = path__namespace.basename(resolvedConfigDir);
1384
+ const startingDir = path__namespace.dirname(resolvedConfigDir);
1385
+ logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
1386
+ const hierarchicalResult = await loadHierarchicalConfig({
1387
+ configDirName,
1388
+ configFileName: options.defaults.configFile,
1389
+ startingDir,
1390
+ encoding: options.defaults.encoding,
1391
+ logger,
1392
+ pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
1393
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
1394
+ fieldOverlaps: options.defaults.fieldOverlaps
1395
+ });
1396
+ rawFileConfig = hierarchicalResult.config;
1397
+ discoveredDirs = hierarchicalResult.discoveredDirs;
1398
+ resolvedConfigDirs = hierarchicalResult.resolvedConfigDirs;
1399
+ // Build detailed source tracking by re-loading each config individually
1400
+ const trackers = [];
1401
+ // Sort by level (highest level first = lowest precedence first) to match merge order
1402
+ const sortedDirs = [
1403
+ ...resolvedConfigDirs
1404
+ ].sort((a, b)=>b.level - a.level);
1405
+ for (const dir of sortedDirs){
1406
+ const storage = create$1({
1407
+ log: logger.debug
1408
+ });
1409
+ const configFilePath = path__namespace.join(dir.path, options.defaults.configFile);
1410
+ try {
1411
+ const exists = await storage.exists(configFilePath);
1412
+ if (!exists) continue;
1413
+ const isReadable = await storage.isFileReadable(configFilePath);
1414
+ if (!isReadable) continue;
1415
+ const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
1416
+ const parsedYaml = yaml__namespace.load(yamlContent);
1417
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
1418
+ const levelTracker = trackConfigSources(parsedYaml, configFilePath, dir.level);
1419
+ trackers.push(levelTracker);
1420
+ }
1421
+ } catch (error) {
1422
+ logger.debug(`Error loading config for source tracking from ${configFilePath}: ${error.message}`);
1423
+ }
1424
+ }
1425
+ // Merge trackers with proper precedence
1426
+ tracker = mergeConfigTrackers(trackers);
1427
+ if (hierarchicalResult.errors.length > 0) {
1428
+ logger.warn('Configuration loading warnings:');
1429
+ hierarchicalResult.errors.forEach((error)=>logger.warn(` ${error}`));
1430
+ }
1431
+ } catch (error) {
1432
+ logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
1433
+ logger.verbose('Falling back to single directory configuration loading');
1434
+ // Fall back to single directory mode for source tracking
1435
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
1436
+ const configFilePath = path__namespace.join(resolvedConfigDir, options.defaults.configFile);
1437
+ tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
1438
+ // Include the directory in discovered, and in resolved only if it had config
1439
+ discoveredDirs = [
1440
+ {
1441
+ path: resolvedConfigDir,
1442
+ level: 0
1443
+ }
1444
+ ];
1445
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
1446
+ resolvedConfigDirs = [
1447
+ {
1448
+ path: resolvedConfigDir,
1449
+ level: 0
1450
+ }
1451
+ ];
1452
+ } else {
1453
+ resolvedConfigDirs = [];
1454
+ }
1455
+ }
1456
+ } else {
1457
+ // Use traditional single directory configuration loading
1458
+ logger.verbose('Using single directory configuration loading for source tracking');
1459
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
1460
+ const configFilePath = path__namespace.join(resolvedConfigDir, options.defaults.configFile);
1461
+ tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
1462
+ // Include the directory in discovered, and in resolved only if it had config
1463
+ discoveredDirs = [
1464
+ {
1465
+ path: resolvedConfigDir,
1466
+ level: 0
1467
+ }
1468
+ ];
1469
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
1470
+ resolvedConfigDirs = [
1471
+ {
1472
+ path: resolvedConfigDir,
1473
+ level: 0
1474
+ }
1475
+ ];
1476
+ } else {
1477
+ resolvedConfigDirs = [];
1478
+ }
1479
+ }
1480
+ // Apply path resolution if configured (this doesn't change source tracking)
1481
+ let processedConfig = rawFileConfig;
1482
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
1483
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
1484
+ }
1485
+ // Build final configuration including built-in values
1486
+ const finalConfig = clean({
1487
+ ...processedConfig,
1488
+ configDirectory: resolvedConfigDir,
1489
+ discoveredConfigDirs: discoveredDirs.map((dir)=>dir.path),
1490
+ resolvedConfigDirs: resolvedConfigDirs.map((dir)=>dir.path)
1491
+ });
1492
+ // Add built-in configuration to tracker
1493
+ tracker['configDirectory'] = {
1494
+ value: resolvedConfigDir,
1495
+ sourcePath: 'built-in',
1496
+ level: -1,
1497
+ sourceLabel: 'Built-in (runtime)'
1498
+ };
1499
+ tracker['discoveredConfigDirs'] = {
1500
+ value: discoveredDirs.map((dir)=>dir.path),
1501
+ sourcePath: 'built-in',
1502
+ level: -1,
1503
+ sourceLabel: 'Built-in (runtime)'
1504
+ };
1505
+ tracker['resolvedConfigDirs'] = {
1506
+ value: resolvedConfigDirs.map((dir)=>dir.path),
1507
+ sourcePath: 'built-in',
1508
+ level: -1,
1509
+ sourceLabel: 'Built-in (runtime)'
1510
+ };
1511
+ // Display the configuration with source information
1512
+ displayConfigWithSources(finalConfig, tracker, discoveredDirs, logger);
1513
+ };
1514
+
1515
+ function _define_property(obj, key, value) {
1516
+ if (key in obj) {
1517
+ Object.defineProperty(obj, key, {
1518
+ value: value,
1519
+ enumerable: true,
1520
+ configurable: true,
1521
+ writable: true
1522
+ });
1523
+ } else {
1524
+ obj[key] = value;
1525
+ }
1526
+ return obj;
1527
+ }
1528
+ /**
1529
+ * Error thrown when configuration validation fails
1530
+ */ class ConfigurationError extends Error {
1531
+ /**
1532
+ * Creates a validation error for when config doesn't match the schema
1533
+ */ static validation(message, zodError, configPath) {
1534
+ return new ConfigurationError('validation', message, zodError, configPath);
1535
+ }
1536
+ /**
1537
+ * Creates an error for when extra/unknown keys are found
1538
+ */ static extraKeys(extraKeys, allowedKeys, configPath) {
1539
+ const message = `Unknown configuration keys found: ${extraKeys.join(', ')}. Allowed keys are: ${allowedKeys.join(', ')}`;
1540
+ return new ConfigurationError('extra_keys', message, {
1541
+ extraKeys,
1542
+ allowedKeys
1543
+ }, configPath);
1544
+ }
1545
+ /**
1546
+ * Creates a schema error for when the configuration schema itself is invalid
1547
+ */ static schema(message, details) {
1548
+ return new ConfigurationError('schema', message, details);
1549
+ }
1550
+ constructor(errorType, message, details, configPath){
1551
+ super(message), _define_property(this, "errorType", void 0), _define_property(this, "details", void 0), _define_property(this, "configPath", void 0);
1552
+ this.name = 'ConfigurationError';
1553
+ this.errorType = errorType;
1554
+ this.details = details;
1555
+ this.configPath = configPath;
1556
+ }
1557
+ }
1558
+
1559
+ /**
1560
+ * Supported configuration file formats.
1561
+ *
1562
+ * - 'yaml': YAML format (.yaml, .yml)
1563
+ * - 'json': JSON format (.json)
1564
+ * - 'javascript': JavaScript module (.js, .mjs, .cjs)
1565
+ * - 'typescript': TypeScript module (.ts, .mts, .cts)
1566
+ */ var ConfigFormat = /*#__PURE__*/ function(ConfigFormat) {
1567
+ ConfigFormat["YAML"] = "yaml";
1568
+ ConfigFormat["JSON"] = "json";
1569
+ ConfigFormat["JavaScript"] = "javascript";
1570
+ ConfigFormat["TypeScript"] = "typescript";
1571
+ return ConfigFormat;
1572
+ }({});
1573
+ /**
1574
+ * Base Zod schema for core Cardigantime configuration.
1575
+ * Contains the minimum required configuration fields.
1576
+ */ const ConfigSchema = zod.z.object({
1577
+ /** The resolved configuration directory path */ configDirectory: zod.z.string(),
1578
+ /** Array of all directory paths that were discovered during hierarchical search */ discoveredConfigDirs: zod.z.array(zod.z.string()),
1579
+ /** Array of directory paths that actually contained valid configuration files */ resolvedConfigDirs: zod.z.array(zod.z.string())
1580
+ });
1581
+ /**
1582
+ * Default root markers used when none are specified.
1583
+ * These indicate common project root boundaries.
1584
+ */ const DEFAULT_ROOT_MARKERS = [
1585
+ {
1586
+ type: 'file',
1587
+ name: 'package.json'
1588
+ },
1589
+ {
1590
+ type: 'directory',
1591
+ name: '.git'
1592
+ },
1593
+ {
1594
+ type: 'file',
1595
+ name: 'pnpm-workspace.yaml'
1596
+ },
1597
+ {
1598
+ type: 'file',
1599
+ name: 'lerna.json'
1600
+ },
1601
+ {
1602
+ type: 'file',
1603
+ name: 'nx.json'
1604
+ },
1605
+ {
1606
+ type: 'file',
1607
+ name: 'rush.json'
1608
+ }
1609
+ ];
1610
+
1611
+ /**
1612
+ * Recursively extracts all keys from a Zod schema in dot notation.
1613
+ *
1614
+ * This function traverses a Zod schema structure and builds a flat list
1615
+ * of all possible keys, using dot notation for nested objects. It handles
1616
+ * optional/nullable types by unwrapping them and supports arrays by
1617
+ * introspecting their element type.
1618
+ *
1619
+ * Special handling for:
1620
+ * - ZodOptional/ZodNullable: Unwraps to get the underlying type
1621
+ * - ZodAny/ZodRecord: Accepts any keys, returns the prefix or empty array
1622
+ * - ZodArray: Introspects the element type
1623
+ * - ZodObject: Recursively processes all shape properties
1624
+ *
1625
+ * @param schema - The Zod schema to introspect
1626
+ * @param prefix - Internal parameter for building nested key paths
1627
+ * @returns Array of strings representing all possible keys in dot notation
1628
+ *
1629
+ * @example
1630
+ * ```typescript
1631
+ * const schema = z.object({
1632
+ * user: z.object({
1633
+ * name: z.string(),
1634
+ * settings: z.object({ theme: z.string() })
1635
+ * }),
1636
+ * debug: z.boolean()
1637
+ * });
1638
+ *
1639
+ * const keys = listZodKeys(schema);
1640
+ * // Returns: ['user.name', 'user.settings.theme', 'debug']
1641
+ * ```
1642
+ */ const listZodKeys = (schema, prefix = '')=>{
1643
+ // Handle ZodOptional and ZodNullable - unwrap to get the underlying type
1644
+ if (schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodNullable) {
1645
+ return listZodKeys(schema.unwrap(), prefix);
1646
+ }
1647
+ // Handle ZodAny and ZodRecord - these accept any keys, so don't introspect
1648
+ if (schema instanceof zod.z.ZodAny || schema instanceof zod.z.ZodRecord) {
1649
+ return prefix ? [
1650
+ prefix
1651
+ ] : [];
1652
+ }
1653
+ if (schema instanceof zod.z.ZodArray) {
1654
+ return listZodKeys(schema.element, prefix);
1655
+ }
1656
+ if (schema instanceof zod.z.ZodObject) {
1657
+ return Object.entries(schema.shape).flatMap(([key, subschema])=>{
1658
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1659
+ const nested = listZodKeys(subschema, fullKey);
1660
+ return nested.length ? nested : fullKey;
1661
+ });
1662
+ }
1663
+ return [];
1664
+ };
1665
+ /**
1666
+ * Type guard to check if a value is a plain object (not array, null, or other types).
1667
+ *
1668
+ * @param value - The value to check
1669
+ * @returns True if the value is a plain object
1670
+ */ const isPlainObject = (value)=>{
1671
+ // Check if it's an object, not null, and not an array.
1672
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1673
+ };
1674
+ /**
1675
+ * Generates a list of all keys within a JavaScript object, using dot notation for nested keys.
1676
+ * Mimics the behavior of listZodKeys but operates on plain objects.
1677
+ * For arrays, it inspects the first element that is a plain object to determine nested keys.
1678
+ * If an array contains no plain objects, or is empty, the key for the array itself is listed.
1679
+ *
1680
+ * @param obj The object to introspect.
1681
+ * @param prefix Internal use for recursion: the prefix for the current nesting level.
1682
+ * @returns An array of strings representing all keys in dot notation.
1683
+ */ const listObjectKeys = (obj, prefix = '')=>{
1684
+ const keys = new Set(); // Use Set to automatically handle duplicates from array recursion
1685
+ for(const key in obj){
1686
+ // Ensure it's an own property, not from the prototype chain
1687
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1688
+ const value = obj[key];
1689
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1690
+ if (Array.isArray(value)) {
1691
+ // Find the first element that is a plain object to determine structure
1692
+ const firstObjectElement = value.find(isPlainObject);
1693
+ if (firstObjectElement) {
1694
+ // Recurse into the structure of the first object element found
1695
+ const nestedKeys = listObjectKeys(firstObjectElement, fullKey);
1696
+ nestedKeys.forEach((k)=>keys.add(k));
1697
+ } else {
1698
+ // Array is empty or contains no plain objects, list the array key itself
1699
+ keys.add(fullKey);
1700
+ }
1701
+ } else if (isPlainObject(value)) {
1702
+ // Recurse into nested plain objects
1703
+ const nestedKeys = listObjectKeys(value, fullKey);
1704
+ nestedKeys.forEach((k)=>keys.add(k));
1705
+ } else {
1706
+ // It's a primitive, null, or other non-plain object/array type
1707
+ keys.add(fullKey);
1708
+ }
1709
+ }
1710
+ }
1711
+ return Array.from(keys); // Convert Set back to Array
1712
+ };
1713
+ /**
1714
+ * Validates that the configuration object contains only keys allowed by the schema.
1715
+ *
1716
+ * This function prevents configuration errors by detecting typos or extra keys
1717
+ * that aren't defined in the Zod schema. It intelligently handles:
1718
+ * - ZodRecord types that accept arbitrary keys
1719
+ * - Nested objects and their key structures
1720
+ * - Arrays and their element key structures
1721
+ *
1722
+ * The function throws a ConfigurationError if extra keys are found, providing
1723
+ * helpful information about what keys are allowed vs. what was found.
1724
+ *
1725
+ * @param mergedSources - The merged configuration object to validate
1726
+ * @param fullSchema - The complete Zod schema including base and user schemas
1727
+ * @param logger - Logger for error reporting
1728
+ * @throws {ConfigurationError} When extra keys are found that aren't in the schema
1729
+ *
1730
+ * @example
1731
+ * ```typescript
1732
+ * const schema = z.object({ name: z.string(), age: z.number() });
1733
+ * const config = { name: 'John', age: 30, typo: 'invalid' };
1734
+ *
1735
+ * checkForExtraKeys(config, schema, console);
1736
+ * // Throws: ConfigurationError with details about 'typo' being an extra key
1737
+ * ```
1738
+ */ const checkForExtraKeys = (mergedSources, fullSchema, logger)=>{
1739
+ const allowedKeys = new Set(listZodKeys(fullSchema));
1740
+ const actualKeys = listObjectKeys(mergedSources);
1741
+ // Filter out keys that are under a record type (ZodRecord accepts any keys)
1742
+ const recordPrefixes = new Set();
1743
+ // Find all prefixes that are ZodRecord types
1744
+ const findRecordPrefixes = (schema, prefix = '')=>{
1745
+ if (schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodNullable) {
1746
+ findRecordPrefixes(schema.unwrap(), prefix);
1747
+ return;
1748
+ }
1749
+ if (schema instanceof zod.z.ZodAny || schema instanceof zod.z.ZodRecord) {
1750
+ if (prefix) recordPrefixes.add(prefix);
1751
+ return;
1752
+ }
1753
+ if (schema instanceof zod.z.ZodObject) {
1754
+ Object.entries(schema.shape).forEach(([key, subschema])=>{
1755
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1756
+ findRecordPrefixes(subschema, fullKey);
1757
+ });
1758
+ }
1759
+ };
1760
+ findRecordPrefixes(fullSchema);
1761
+ // Filter out keys that are under record prefixes
1762
+ const extraKeys = actualKeys.filter((key)=>{
1763
+ if (allowedKeys.has(key)) return false;
1764
+ // Check if this key is under a record prefix
1765
+ for (const recordPrefix of recordPrefixes){
1766
+ if (key.startsWith(recordPrefix + '.')) {
1767
+ return false; // This key is allowed under a record
1768
+ }
1769
+ }
1770
+ return true; // This is an extra key
1771
+ });
1772
+ if (extraKeys.length > 0) {
1773
+ const allowedKeysArray = Array.from(allowedKeys);
1774
+ const error = ConfigurationError.extraKeys(extraKeys, allowedKeysArray);
1775
+ logger.error(error.message);
1776
+ throw error;
1777
+ }
1778
+ };
1779
+ /**
1780
+ * Validates that a configuration directory exists and is accessible.
1781
+ *
1782
+ * This function performs file system checks to ensure the configuration
1783
+ * directory can be used. It handles the isRequired flag to determine
1784
+ * whether a missing directory should cause an error or be silently ignored.
1785
+ *
1786
+ * @param configDirectory - Path to the configuration directory
1787
+ * @param isRequired - Whether the directory must exist
1788
+ * @param logger - Optional logger for debug information
1789
+ * @throws {FileSystemError} When the directory is required but missing or unreadable
1790
+ */ const validateConfigDirectory = async (configDirectory, isRequired, logger)=>{
1791
+ const storage = create$1({
1792
+ log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
1793
+ });
1794
+ const exists = await storage.exists(configDirectory);
1795
+ if (!exists) {
1796
+ if (isRequired) {
1797
+ throw FileSystemError.directoryNotFound(configDirectory, true);
1798
+ }
1799
+ } else if (exists) {
1800
+ const isReadable = await storage.isDirectoryReadable(configDirectory);
1801
+ if (!isReadable) {
1802
+ throw FileSystemError.directoryNotReadable(configDirectory);
1803
+ }
1804
+ }
1805
+ };
1806
+ /**
1807
+ * Validates a configuration object against the combined Zod schema.
1808
+ *
1809
+ * This is the main validation function that:
1810
+ * 1. Validates the configuration directory (if config feature enabled)
1811
+ * 2. Combines the base ConfigSchema with user-provided schema shape
1812
+ * 3. Checks for extra keys not defined in the schema
1813
+ * 4. Validates all values against their schema definitions
1814
+ * 5. Provides detailed error reporting for validation failures
1815
+ *
1816
+ * The validation is comprehensive and catches common configuration errors
1817
+ * including typos, missing required fields, wrong types, and invalid values.
1818
+ *
1819
+ * @template T - The Zod schema shape type for configuration validation
1820
+ * @param config - The merged configuration object to validate
1821
+ * @param options - Cardigantime options containing schema, defaults, and logger
1822
+ * @throws {ConfigurationError} When configuration validation fails
1823
+ * @throws {FileSystemError} When configuration directory validation fails
1824
+ *
1825
+ * @example
1826
+ * ```typescript
1827
+ * const schema = z.object({
1828
+ * apiKey: z.string().min(1),
1829
+ * timeout: z.number().positive(),
1830
+ * });
1831
+ *
1832
+ * await validate(config, {
1833
+ * configShape: schema.shape,
1834
+ * defaults: { configDirectory: './config', isRequired: true },
1835
+ * logger: console,
1836
+ * features: ['config']
1837
+ * });
1838
+ * // Throws detailed errors if validation fails
1839
+ * ```
1840
+ */ const validate = async (config, options)=>{
1841
+ const logger = options.logger;
1842
+ if (options.features.includes('config') && config.configDirectory) {
1843
+ await validateConfigDirectory(config.configDirectory, options.defaults.isRequired, logger);
1844
+ }
1845
+ // Combine the base schema with the user-provided shape
1846
+ const fullSchema = zod.z.object({
1847
+ ...ConfigSchema.shape,
1848
+ ...options.configShape
1849
+ });
1850
+ // Validate the merged sources against the full schema
1851
+ const validationResult = fullSchema.safeParse(config);
1852
+ // Check for extraneous keys
1853
+ checkForExtraKeys(config, fullSchema, logger);
1854
+ if (!validationResult.success) {
1855
+ const formattedError = JSON.stringify(validationResult.error.format(), null, 2);
1856
+ logger.error('Configuration validation failed. Check logs for details.');
1857
+ logger.silly('Configuration validation failed: %s', formattedError);
1858
+ throw ConfigurationError.validation('Configuration validation failed. Check logs for details.', validationResult.error);
1859
+ }
1860
+ return;
1861
+ };
1862
+
1863
+ /**
1864
+ * Extracts default values from a Zod schema recursively using Zod v4's parsing mechanisms.
1865
+ *
1866
+ * This function leverages Zod's own parsing behavior to extract defaults rather than
1867
+ * accessing internal properties. It works by:
1868
+ * 1. For ZodDefault types: parsing undefined to trigger the default
1869
+ * 2. For ZodObject types: creating a minimal object and parsing to get all defaults
1870
+ * 3. For wrapped types: unwrapping and recursing
1871
+ *
1872
+ * @param schema - The Zod schema to extract defaults from
1873
+ * @returns An object containing all default values from the schema
1874
+ *
1875
+ * @example
1876
+ * ```typescript
1877
+ * const schema = z.object({
1878
+ * name: z.string().default('app'),
1879
+ * port: z.number().default(3000),
1880
+ * debug: z.boolean().default(false),
1881
+ * database: z.object({
1882
+ * host: z.string().default('localhost'),
1883
+ * port: z.number().default(5432)
1884
+ * })
1885
+ * });
1886
+ *
1887
+ * const defaults = extractSchemaDefaults(schema);
1888
+ * // Returns: { name: 'app', port: 3000, debug: false, database: { host: 'localhost', port: 5432 } }
1889
+ * ```
1890
+ */ const extractSchemaDefaults = (schema)=>{
1891
+ // Handle ZodDefault - parse undefined to get the default value
1892
+ if (schema instanceof zod.z.ZodDefault) {
1893
+ try {
1894
+ return schema.parse(undefined);
1895
+ } catch {
1896
+ // If parsing undefined fails, return undefined
1897
+ return undefined;
1898
+ }
1899
+ }
1900
+ // Handle ZodOptional and ZodNullable by unwrapping
1901
+ if (schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodNullable) {
1902
+ return extractSchemaDefaults(schema.unwrap());
1903
+ }
1904
+ // Handle ZodObject - create an object with defaults by parsing an empty object
1905
+ if (schema instanceof zod.z.ZodObject) {
1906
+ const defaults = {};
1907
+ const shape = schema.shape;
1908
+ // First, try to extract defaults from individual fields
1909
+ for (const [key, subschema] of Object.entries(shape)){
1910
+ const defaultValue = extractSchemaDefaults(subschema);
1911
+ if (defaultValue !== undefined) {
1912
+ defaults[key] = defaultValue;
1913
+ }
1914
+ }
1915
+ // Then parse an empty object to trigger any schema-level defaults
1916
+ const result = schema.safeParse({});
1917
+ if (result.success) {
1918
+ // Merge the parsed result with our extracted defaults
1919
+ return {
1920
+ ...defaults,
1921
+ ...result.data
1922
+ };
1923
+ }
1924
+ return Object.keys(defaults).length > 0 ? defaults : undefined;
1925
+ }
1926
+ // Handle ZodArray - return empty array as a reasonable default
1927
+ if (schema instanceof zod.z.ZodArray) {
1928
+ const elementDefaults = extractSchemaDefaults(schema.element);
1929
+ return elementDefaults !== undefined ? [
1930
+ elementDefaults
1931
+ ] : [];
1932
+ }
1933
+ // Handle ZodRecord - return empty object as default
1934
+ if (schema instanceof zod.z.ZodRecord) {
1935
+ return {};
1936
+ }
1937
+ // No default available for other schema types
1938
+ return undefined;
1939
+ };
1940
+ /**
1941
+ * Generates a complete configuration object with all default values populated.
1942
+ *
1943
+ * This function combines the base ConfigSchema with a user-provided schema shape
1944
+ * and extracts all available default values to create a complete configuration
1945
+ * example that can be serialized to YAML.
1946
+ *
1947
+ * @template T - The Zod schema shape type
1948
+ * @param configShape - The user's configuration schema shape
1949
+ * @param configDirectory - The configuration directory to include in the defaults
1950
+ * @returns An object containing all default values suitable for YAML serialization
1951
+ *
1952
+ * @example
1953
+ * ```typescript
1954
+ * const shape = z.object({
1955
+ * apiKey: z.string().describe('Your API key'),
1956
+ * timeout: z.number().default(5000).describe('Request timeout in milliseconds'),
1957
+ * features: z.array(z.string()).default(['auth', 'logging'])
1958
+ * }).shape;
1959
+ *
1960
+ * const config = generateDefaultConfig(shape, './config');
1961
+ * // Returns: { timeout: 5000, features: ['auth', 'logging'] }
1962
+ * // Note: apiKey is not included since it has no default
1963
+ * ```
1964
+ */ const generateDefaultConfig = (configShape, _configDirectory)=>{
1965
+ // Create the full schema by combining base and user schema
1966
+ const fullSchema = zod.z.object({
1967
+ ...configShape
1968
+ });
1969
+ // Extract defaults from the full schema using only explicit defaults
1970
+ const defaults = extractSchemaDefaults(fullSchema);
1971
+ // Don't include configDirectory in the generated file since it's runtime-specific
1972
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1973
+ const { configDirectory: _, ...configDefaults } = defaults || {};
1974
+ return configDefaults || {};
1975
+ };
1976
+
1977
+ /**
1978
+ * Creates a new Cardigantime instance for configuration management.
1979
+ *
1980
+ * Cardigantime handles the complete configuration lifecycle including:
1981
+ * - Reading configuration from YAML files
1982
+ * - Validating configuration against Zod schemas
1983
+ * - Merging CLI arguments with file configuration and defaults
1984
+ * - Providing type-safe configuration objects
1985
+ *
1986
+ * @template T - The Zod schema shape type for your configuration
1987
+ * @param pOptions - Configuration options for the Cardigantime instance
1988
+ * @param pOptions.defaults - Default configuration settings
1989
+ * @param pOptions.defaults.configDirectory - Directory to search for configuration files (required)
1990
+ * @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
1991
+ * @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
1992
+ * @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
1993
+ * @param pOptions.defaults.pathResolution - Configuration for resolving relative paths in config values relative to the config file's directory (optional)
1994
+ * @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
1995
+ * @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
1996
+ * @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
1997
+ * @returns A Cardigantime instance with methods for configure, read, validate, and setLogger
1998
+ *
1999
+ * @example
2000
+ * ```typescript
2001
+ * import { create } from '@utilarium/cardigantime';
2002
+ * import { z } from 'zod';
2003
+ *
2004
+ * const MyConfigSchema = z.object({
2005
+ * apiKey: z.string().min(1),
2006
+ * timeout: z.number().default(5000),
2007
+ * debug: z.boolean().default(false),
2008
+ * contextDirectories: z.array(z.string()).optional(),
2009
+ * });
2010
+ *
2011
+ * const cardigantime = create({
2012
+ * defaults: {
2013
+ * configDirectory: './config',
2014
+ * configFile: 'myapp.yaml',
2015
+ * // Resolve relative paths in contextDirectories relative to config file location
2016
+ * pathResolution: {
2017
+ * pathFields: ['contextDirectories'],
2018
+ * resolvePathArray: ['contextDirectories']
2019
+ * },
2020
+ * // Configure how array fields are merged in hierarchical mode
2021
+ * fieldOverlaps: {
2022
+ * 'features': 'append', // Accumulate features from all levels
2023
+ * 'excludePatterns': 'prepend' // Higher precedence patterns come first
2024
+ * }
2025
+ * },
2026
+ * configShape: MyConfigSchema.shape,
2027
+ * features: ['config', 'hierarchical'], // Enable hierarchical discovery
2028
+ * });
2029
+ *
2030
+ * // If config file is at ../config/myapp.yaml and contains:
2031
+ * // contextDirectories: ['./context', './data']
2032
+ * // These paths will be resolved relative to ../config/ directory
2033
+ * ```
2034
+ */ const create = (pOptions)=>{
2035
+ // Validate that configDirectory is a string
2036
+ if (!pOptions.defaults.configDirectory || typeof pOptions.defaults.configDirectory !== 'string') {
2037
+ throw new Error(`Configuration directory must be a string, received: ${typeof pOptions.defaults.configDirectory} (${JSON.stringify(pOptions.defaults.configDirectory)})`);
2038
+ }
2039
+ const defaults = {
2040
+ ...DEFAULT_OPTIONS,
2041
+ ...pOptions.defaults
2042
+ };
2043
+ const features = pOptions.features || DEFAULT_FEATURES;
2044
+ const configShape = pOptions.configShape;
2045
+ let logger = pOptions.logger || DEFAULT_LOGGER;
2046
+ const options = {
2047
+ defaults,
2048
+ features,
2049
+ configShape,
2050
+ logger
2051
+ };
2052
+ const setLogger = (pLogger)=>{
2053
+ logger = pLogger;
2054
+ options.logger = pLogger;
2055
+ };
2056
+ const generateConfig = async (configDirectory)=>{
2057
+ const targetDir = configDirectory || options.defaults.configDirectory;
2058
+ const configFile = options.defaults.configFile;
2059
+ const encoding = options.defaults.encoding;
2060
+ // Validate that targetDir is a string
2061
+ if (!targetDir || typeof targetDir !== 'string') {
2062
+ throw new Error(`Configuration directory must be a string, received: ${typeof targetDir} (${JSON.stringify(targetDir)})`);
2063
+ }
2064
+ logger.verbose(`Generating configuration file in: ${targetDir}`);
2065
+ // Create storage utility
2066
+ const storage = create$1({
2067
+ log: logger.debug
2068
+ });
2069
+ // Ensure the target directory exists
2070
+ const dirExists = await storage.exists(targetDir);
2071
+ if (!dirExists) {
2072
+ logger.info(`Creating configuration directory: ${targetDir}`);
2073
+ try {
2074
+ await storage.createDirectory(targetDir);
2075
+ } catch (error) {
2076
+ throw FileSystemError.directoryCreationFailed(targetDir, error);
2077
+ }
2078
+ }
2079
+ // Check if directory is writable
2080
+ const isWritable = await storage.isDirectoryWritable(targetDir);
2081
+ if (!isWritable) {
2082
+ throw new FileSystemError('not_writable', 'Configuration directory is not writable', targetDir, 'directory_write');
2083
+ }
2084
+ // Build the full config file path
2085
+ const configFilePath = path__namespace.join(targetDir, configFile);
2086
+ // Generate default configuration
2087
+ logger.debug(`Generating defaults for schema with keys: ${Object.keys(options.configShape).join(', ')}`);
2088
+ const defaultConfig = generateDefaultConfig(options.configShape);
2089
+ logger.debug(`Generated default config: ${JSON.stringify(defaultConfig, null, 2)}`);
2090
+ // Convert to YAML with nice formatting
2091
+ const yamlContent = yaml__namespace.dump(defaultConfig, {
2092
+ indent: 2,
2093
+ lineWidth: 120,
2094
+ noRefs: true,
2095
+ sortKeys: true
2096
+ });
2097
+ // Add header comment to the YAML file
2098
+ const header = `# Configuration file generated by Cardigantime
2099
+ # This file contains default values for your application configuration.
2100
+ # Modify the values below to customize your application's behavior.
2101
+ #
2102
+ # For more information about Cardigantime configuration:
2103
+ # https://utilarium.github.io/cardigantime/
2104
+
2105
+ `;
2106
+ const finalContent = header + yamlContent;
2107
+ // Check if config file already exists
2108
+ const configExists = await storage.exists(configFilePath);
2109
+ if (configExists) {
2110
+ logger.warn(`Configuration file already exists: ${configFilePath}`);
2111
+ logger.warn('This file was not overwritten, but here is what the default configuration looks like if you want to copy it:');
2112
+ logger.info('\n' + '='.repeat(60));
2113
+ logger.info(finalContent.trim());
2114
+ logger.info('='.repeat(60));
2115
+ return;
2116
+ }
2117
+ // Write the configuration file
2118
+ try {
2119
+ await storage.writeFile(configFilePath, finalContent, encoding);
2120
+ logger.info(`Configuration file generated successfully: ${configFilePath}`);
2121
+ } catch (error) {
2122
+ throw FileSystemError.operationFailed('write configuration file', configFilePath, error);
2123
+ }
2124
+ };
2125
+ return {
2126
+ setLogger,
2127
+ configure: (command)=>configure(command, options),
2128
+ validate: (config)=>validate(config, options),
2129
+ read: (args)=>read(args, options),
2130
+ generateConfig,
2131
+ checkConfig: (args)=>checkConfig(args, options)
2132
+ };
2133
+ };
2134
+ /**
2135
+ * Type-safe helper for defining configuration in TypeScript/JavaScript files.
2136
+ *
2137
+ * This is a simple identity function that provides type checking and
2138
+ * autocomplete for configuration objects when using TypeScript config files.
2139
+ *
2140
+ * @template T - The configuration type
2141
+ * @param config - The configuration object
2142
+ * @returns The same configuration object (identity function)
2143
+ *
2144
+ * @example
2145
+ * ```typescript
2146
+ * // config.ts
2147
+ * import { defineConfig } from '@utilarium/cardigantime';
2148
+ *
2149
+ * export default defineConfig({
2150
+ * apiKey: process.env.API_KEY || 'default-key',
2151
+ * timeout: 5000,
2152
+ * debug: process.env.NODE_ENV === 'development'
2153
+ * });
2154
+ * ```
2155
+ */ function defineConfig(config) {
2156
+ return config;
2157
+ }
2158
+
2159
+ exports.ArgumentError = ArgumentError;
2160
+ exports.ConfigFormat = ConfigFormat;
2161
+ exports.ConfigSchema = ConfigSchema;
2162
+ exports.ConfigurationError = ConfigurationError;
2163
+ exports.DEFAULT_ROOT_MARKERS = DEFAULT_ROOT_MARKERS;
2164
+ exports.FileSystemError = FileSystemError;
2165
+ exports.PROGRAM_NAME = PROGRAM_NAME;
2166
+ exports.VERSION = VERSION;
2167
+ exports.create = create;
2168
+ exports.defineConfig = defineConfig;
2169
+ //# sourceMappingURL=cardigantime.cjs.map