@theunwalked/cardigantime 0.0.5 → 0.0.6
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.
- package/README.md +225 -4
- package/dist/cardigantime.cjs +311 -11
- package/dist/cardigantime.cjs.map +1 -1
- package/dist/read.js +61 -11
- package/dist/read.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.js.map +1 -1
- package/dist/util/hierarchical.d.ts +120 -0
- package/dist/util/hierarchical.js +257 -0
- package/dist/util/hierarchical.js.map +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ debug: true
|
|
|
240
240
|
Cardigantime merges configuration from multiple sources in this order (highest to lowest priority):
|
|
241
241
|
|
|
242
242
|
1. **Command-line arguments** (highest priority)
|
|
243
|
-
2. **Configuration file** (medium priority)
|
|
243
|
+
2. **Configuration file(s)** (medium priority)
|
|
244
244
|
3. **Default values** (lowest priority)
|
|
245
245
|
|
|
246
246
|
```typescript
|
|
@@ -255,7 +255,105 @@ Cardigantime merges configuration from multiple sources in this order (highest t
|
|
|
255
255
|
// debug: false (from file)
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
### 2.
|
|
258
|
+
### 2. Hierarchical Configuration Discovery
|
|
259
|
+
|
|
260
|
+
Cardigantime supports hierarchical configuration discovery, similar to how tools like `.gitignore`, `.eslintrc`, or `package.json` work. When the `hierarchical` feature is enabled, Cardigantime will:
|
|
261
|
+
|
|
262
|
+
1. **Start from the specified config directory** (e.g., `./project/subdir/.kodrdriv`)
|
|
263
|
+
2. **Search up the directory tree** for additional config directories with the same name
|
|
264
|
+
3. **Merge configurations** with proper precedence (closer directories win)
|
|
265
|
+
4. **Apply CLI arguments** as the final override
|
|
266
|
+
|
|
267
|
+
#### Example Directory Structure
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
/home/user/projects/
|
|
271
|
+
├── .kodrdriv/
|
|
272
|
+
│ └── config.yaml # Root-level config
|
|
273
|
+
├── myproject/
|
|
274
|
+
│ ├── .kodrdriv/
|
|
275
|
+
│ │ └── config.yaml # Project-level config
|
|
276
|
+
│ └── submodule/
|
|
277
|
+
│ ├── .kodrdriv/
|
|
278
|
+
│ │ └── config.yaml # Submodule-level config
|
|
279
|
+
│ └── my-script.js
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Hierarchical Discovery Behavior
|
|
283
|
+
|
|
284
|
+
When running from `/home/user/projects/myproject/submodule/` with hierarchical discovery:
|
|
285
|
+
|
|
286
|
+
1. **Level 0 (Highest Priority)**: `/home/user/projects/myproject/submodule/.kodrdriv/config.yaml`
|
|
287
|
+
2. **Level 1**: `/home/user/projects/myproject/.kodrdriv/config.yaml`
|
|
288
|
+
3. **Level 2 (Lowest Priority)**: `/home/user/projects/.kodrdriv/config.yaml`
|
|
289
|
+
|
|
290
|
+
Configurations are deep-merged, with closer directories taking precedence:
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
# /home/user/projects/.kodrdriv/config.yaml (Level 2)
|
|
294
|
+
database:
|
|
295
|
+
host: localhost
|
|
296
|
+
port: 5432
|
|
297
|
+
ssl: false
|
|
298
|
+
logging:
|
|
299
|
+
level: info
|
|
300
|
+
|
|
301
|
+
# /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
|
|
302
|
+
database:
|
|
303
|
+
port: 5433
|
|
304
|
+
ssl: true
|
|
305
|
+
api:
|
|
306
|
+
timeout: 5000
|
|
307
|
+
|
|
308
|
+
# /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
|
|
309
|
+
database:
|
|
310
|
+
host: dev.example.com
|
|
311
|
+
logging:
|
|
312
|
+
level: debug
|
|
313
|
+
|
|
314
|
+
# Final merged configuration:
|
|
315
|
+
database:
|
|
316
|
+
host: dev.example.com # From Level 0 (highest precedence)
|
|
317
|
+
port: 5433 # From Level 1
|
|
318
|
+
ssl: true # From Level 1
|
|
319
|
+
api:
|
|
320
|
+
timeout: 5000 # From Level 1
|
|
321
|
+
logging:
|
|
322
|
+
level: debug # From Level 0 (highest precedence)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### Enabling Hierarchical Discovery
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const cardigantime = create({
|
|
329
|
+
defaults: {
|
|
330
|
+
configDirectory: '.kodrdriv',
|
|
331
|
+
configFile: 'config.yaml'
|
|
332
|
+
},
|
|
333
|
+
configShape: MyConfigSchema.shape,
|
|
334
|
+
features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### Hierarchical Discovery Options
|
|
339
|
+
|
|
340
|
+
The hierarchical discovery has several built-in protections and features:
|
|
341
|
+
|
|
342
|
+
- **Maximum traversal depth**: Prevents infinite loops (default: 10 levels)
|
|
343
|
+
- **Symlink protection**: Tracks visited paths to prevent circular references
|
|
344
|
+
- **Graceful fallback**: Falls back to single-directory mode if discovery fails
|
|
345
|
+
- **Error tolerance**: Continues discovery even if some directories are unreadable
|
|
346
|
+
- **Root detection**: Automatically stops at filesystem root
|
|
347
|
+
|
|
348
|
+
#### Use Cases for Hierarchical Configuration
|
|
349
|
+
|
|
350
|
+
1. **Monorepos**: Share common configuration across multiple packages
|
|
351
|
+
2. **Project inheritance**: Override team/organization defaults for specific projects
|
|
352
|
+
3. **Environment layering**: Different configs for development/staging/production
|
|
353
|
+
4. **Tool configuration**: Similar to how ESLint or Prettier find configs up the tree
|
|
354
|
+
5. **Multi-tenant applications**: Tenant-specific overrides of global settings
|
|
355
|
+
|
|
356
|
+
### 3. Schema Validation
|
|
259
357
|
|
|
260
358
|
All configuration is validated against your Zod schema:
|
|
261
359
|
|
|
@@ -276,7 +374,7 @@ const cardigantime = create({
|
|
|
276
374
|
});
|
|
277
375
|
```
|
|
278
376
|
|
|
279
|
-
###
|
|
377
|
+
### 4. Type Safety
|
|
280
378
|
|
|
281
379
|
Cardigantime provides full TypeScript support:
|
|
282
380
|
|
|
@@ -293,7 +391,7 @@ if (config.features.includes('auth')) {
|
|
|
293
391
|
}
|
|
294
392
|
```
|
|
295
393
|
|
|
296
|
-
###
|
|
394
|
+
### 5. Error Handling
|
|
297
395
|
|
|
298
396
|
Cardigantime provides detailed error messages for common issues:
|
|
299
397
|
|
|
@@ -365,6 +463,129 @@ Sets a custom logger for debugging and error reporting.
|
|
|
365
463
|
|
|
366
464
|
## Advanced Usage
|
|
367
465
|
|
|
466
|
+
### Hierarchical Configuration Discovery
|
|
467
|
+
|
|
468
|
+
Here's a complete example of using hierarchical configuration discovery for a monorepo setup:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { create } from '@theunwalked/cardigantime';
|
|
472
|
+
import { z } from 'zod';
|
|
473
|
+
|
|
474
|
+
// Define a comprehensive configuration schema
|
|
475
|
+
const ProjectConfigSchema = z.object({
|
|
476
|
+
projectName: z.string(),
|
|
477
|
+
environment: z.enum(['development', 'staging', 'production']).default('development'),
|
|
478
|
+
database: z.object({
|
|
479
|
+
host: z.string().default('localhost'),
|
|
480
|
+
port: z.number().default(5432),
|
|
481
|
+
ssl: z.boolean().default(false),
|
|
482
|
+
maxConnections: z.number().default(10),
|
|
483
|
+
}),
|
|
484
|
+
api: z.object({
|
|
485
|
+
baseUrl: z.string().url(),
|
|
486
|
+
timeout: z.number().default(5000),
|
|
487
|
+
retries: z.number().default(3),
|
|
488
|
+
}),
|
|
489
|
+
features: z.record(z.boolean()).default({}),
|
|
490
|
+
logging: z.object({
|
|
491
|
+
level: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
|
|
492
|
+
outputs: z.array(z.string()).default(['console']),
|
|
493
|
+
}),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Enable hierarchical discovery
|
|
497
|
+
const cardigantime = create({
|
|
498
|
+
defaults: {
|
|
499
|
+
configDirectory: '.myapp',
|
|
500
|
+
configFile: 'config.yaml',
|
|
501
|
+
},
|
|
502
|
+
configShape: ProjectConfigSchema.shape,
|
|
503
|
+
features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Usage in a CLI tool
|
|
507
|
+
async function setupProject() {
|
|
508
|
+
try {
|
|
509
|
+
const config = await cardigantime.read(process.argv);
|
|
510
|
+
await cardigantime.validate(config);
|
|
511
|
+
|
|
512
|
+
console.log(`Setting up ${config.projectName} in ${config.environment} mode`);
|
|
513
|
+
console.log(`Database: ${config.database.host}:${config.database.port}`);
|
|
514
|
+
console.log(`API: ${config.api.baseUrl}`);
|
|
515
|
+
|
|
516
|
+
return config;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error('Configuration error:', error.message);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Directory Structure:**
|
|
525
|
+
```
|
|
526
|
+
/workspace/
|
|
527
|
+
├── .myapp/
|
|
528
|
+
│ └── config.yaml # Global defaults
|
|
529
|
+
├── team-frontend/
|
|
530
|
+
│ ├── .myapp/
|
|
531
|
+
│ │ └── config.yaml # Team-specific settings
|
|
532
|
+
│ ├── app1/
|
|
533
|
+
│ │ ├── .myapp/
|
|
534
|
+
│ │ │ └── config.yaml # App-specific overrides
|
|
535
|
+
│ │ └── package.json
|
|
536
|
+
│ └── app2/
|
|
537
|
+
│ └── package.json # Uses team + global config
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Configuration Files:**
|
|
541
|
+
```yaml
|
|
542
|
+
# /workspace/.myapp/config.yaml (Global)
|
|
543
|
+
database:
|
|
544
|
+
host: prod.db.company.com
|
|
545
|
+
ssl: true
|
|
546
|
+
api:
|
|
547
|
+
baseUrl: https://api.company.com
|
|
548
|
+
logging:
|
|
549
|
+
level: warn
|
|
550
|
+
outputs: [console, file]
|
|
551
|
+
|
|
552
|
+
# /workspace/team-frontend/.myapp/config.yaml (Team)
|
|
553
|
+
database:
|
|
554
|
+
host: team-frontend.db.company.com
|
|
555
|
+
api:
|
|
556
|
+
timeout: 3000
|
|
557
|
+
features:
|
|
558
|
+
analytics: true
|
|
559
|
+
darkMode: true
|
|
560
|
+
|
|
561
|
+
# /workspace/team-frontend/app1/.myapp/config.yaml (App)
|
|
562
|
+
projectName: frontend-app1
|
|
563
|
+
environment: development
|
|
564
|
+
database:
|
|
565
|
+
host: localhost # Override for local development
|
|
566
|
+
logging:
|
|
567
|
+
level: debug
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
When running from `/workspace/team-frontend/app1/`, the final merged configuration will be:
|
|
571
|
+
|
|
572
|
+
```yaml
|
|
573
|
+
projectName: frontend-app1 # From app level
|
|
574
|
+
environment: development # From app level
|
|
575
|
+
database:
|
|
576
|
+
host: localhost # From app level (highest precedence)
|
|
577
|
+
ssl: true # From global level
|
|
578
|
+
api:
|
|
579
|
+
baseUrl: https://api.company.com # From global level
|
|
580
|
+
timeout: 3000 # From team level
|
|
581
|
+
features:
|
|
582
|
+
analytics: true # From team level
|
|
583
|
+
darkMode: true # From team level
|
|
584
|
+
logging:
|
|
585
|
+
level: debug # From app level (highest precedence)
|
|
586
|
+
outputs: [console, file] # From global level
|
|
587
|
+
```
|
|
588
|
+
|
|
368
589
|
### Custom Logger
|
|
369
590
|
|
|
370
591
|
```typescript
|
package/dist/cardigantime.cjs
CHANGED
|
@@ -415,6 +415,257 @@ const create$1 = (params)=>{
|
|
|
415
415
|
};
|
|
416
416
|
};
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Discovers configuration directories by traversing up the directory tree.
|
|
420
|
+
*
|
|
421
|
+
* Starting from the specified directory (or current working directory),
|
|
422
|
+
* this function searches for directories with the given name, continuing
|
|
423
|
+
* up the directory tree until it reaches the filesystem root or the
|
|
424
|
+
* maximum number of levels.
|
|
425
|
+
*
|
|
426
|
+
* @param options Configuration options for discovery
|
|
427
|
+
* @returns Promise resolving to array of discovered configuration directories
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```typescript
|
|
431
|
+
* const dirs = await discoverConfigDirectories({
|
|
432
|
+
* configDirName: '.kodrdriv',
|
|
433
|
+
* configFileName: 'config.yaml',
|
|
434
|
+
* maxLevels: 5
|
|
435
|
+
* });
|
|
436
|
+
* // Returns: [
|
|
437
|
+
* // { path: '/project/.kodrdriv', level: 0 },
|
|
438
|
+
* // { path: '/project/parent/.kodrdriv', level: 1 }
|
|
439
|
+
* // ]
|
|
440
|
+
* ```
|
|
441
|
+
*/ async function discoverConfigDirectories(options) {
|
|
442
|
+
const { configDirName, maxLevels = 10, startingDir = process.cwd(), logger } = options;
|
|
443
|
+
const storage = create$1({
|
|
444
|
+
log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
|
|
445
|
+
});
|
|
446
|
+
const discoveredDirs = [];
|
|
447
|
+
let currentDir = path.resolve(startingDir);
|
|
448
|
+
let level = 0;
|
|
449
|
+
const visited = new Set(); // Prevent infinite loops with symlinks
|
|
450
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Starting hierarchical discovery from: ${currentDir}`);
|
|
451
|
+
while(level < maxLevels){
|
|
452
|
+
// Prevent infinite loops with symlinks
|
|
453
|
+
const realPath = path.resolve(currentDir);
|
|
454
|
+
if (visited.has(realPath)) {
|
|
455
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Already visited ${realPath}, stopping discovery`);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
visited.add(realPath);
|
|
459
|
+
const configDirPath = path.join(currentDir, configDirName);
|
|
460
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Checking for config directory: ${configDirPath}`);
|
|
461
|
+
try {
|
|
462
|
+
const exists = await storage.exists(configDirPath);
|
|
463
|
+
const isReadable = exists && await storage.isDirectoryReadable(configDirPath);
|
|
464
|
+
if (exists && isReadable) {
|
|
465
|
+
discoveredDirs.push({
|
|
466
|
+
path: configDirPath,
|
|
467
|
+
level
|
|
468
|
+
});
|
|
469
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Found config directory at level ${level}: ${configDirPath}`);
|
|
470
|
+
} else if (exists && !isReadable) {
|
|
471
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Config directory exists but is not readable: ${configDirPath}`);
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Error checking config directory ${configDirPath}: ${error.message}`);
|
|
475
|
+
}
|
|
476
|
+
// Move up one directory level
|
|
477
|
+
const parentDir = path.dirname(currentDir);
|
|
478
|
+
// Check if we've reached the root directory
|
|
479
|
+
if (parentDir === currentDir) {
|
|
480
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('Reached filesystem root, stopping discovery');
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
currentDir = parentDir;
|
|
484
|
+
level++;
|
|
485
|
+
}
|
|
486
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Discovery complete. Found ${discoveredDirs.length} config directories`);
|
|
487
|
+
return discoveredDirs;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Loads and parses a configuration file from a directory.
|
|
491
|
+
*
|
|
492
|
+
* @param configDir Path to the configuration directory
|
|
493
|
+
* @param configFileName Name of the configuration file
|
|
494
|
+
* @param encoding File encoding
|
|
495
|
+
* @param logger Optional logger
|
|
496
|
+
* @returns Promise resolving to parsed configuration object or null if not found
|
|
497
|
+
*/ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger) {
|
|
498
|
+
const storage = create$1({
|
|
499
|
+
log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
|
|
500
|
+
});
|
|
501
|
+
const configFilePath = path.join(configDir, configFileName);
|
|
502
|
+
try {
|
|
503
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Attempting to load config file: ${configFilePath}`);
|
|
504
|
+
const exists = await storage.exists(configFilePath);
|
|
505
|
+
if (!exists) {
|
|
506
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Config file does not exist: ${configFilePath}`);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const isReadable = await storage.isFileReadable(configFilePath);
|
|
510
|
+
if (!isReadable) {
|
|
511
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Config file exists but is not readable: ${configFilePath}`);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const yamlContent = await storage.readFile(configFilePath, encoding);
|
|
515
|
+
const parsedYaml = yaml__namespace.load(yamlContent);
|
|
516
|
+
if (parsedYaml !== null && typeof parsedYaml === 'object') {
|
|
517
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Successfully loaded config from: ${configFilePath}`);
|
|
518
|
+
return parsedYaml;
|
|
519
|
+
} else {
|
|
520
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Config file contains invalid format: ${configFilePath}`);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Error loading config from ${configFilePath}: ${error.message}`);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Deep merges multiple configuration objects with proper precedence.
|
|
530
|
+
*
|
|
531
|
+
* Objects are merged from lowest precedence to highest precedence,
|
|
532
|
+
* meaning that properties in later objects override properties in earlier objects.
|
|
533
|
+
* Arrays are replaced entirely (not merged).
|
|
534
|
+
*
|
|
535
|
+
* @param configs Array of configuration objects, ordered from lowest to highest precedence
|
|
536
|
+
* @returns Merged configuration object
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* const merged = deepMergeConfigs([
|
|
541
|
+
* { api: { timeout: 5000 }, debug: true }, // Lower precedence
|
|
542
|
+
* { api: { retries: 3 }, features: ['auth'] }, // Higher precedence
|
|
543
|
+
* ]);
|
|
544
|
+
* // Result: { api: { timeout: 5000, retries: 3 }, debug: true, features: ['auth'] }
|
|
545
|
+
* ```
|
|
546
|
+
*/ function deepMergeConfigs(configs) {
|
|
547
|
+
if (configs.length === 0) {
|
|
548
|
+
return {};
|
|
549
|
+
}
|
|
550
|
+
if (configs.length === 1) {
|
|
551
|
+
return {
|
|
552
|
+
...configs[0]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return configs.reduce((merged, current)=>{
|
|
556
|
+
return deepMergeTwo(merged, current);
|
|
557
|
+
}, {});
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Deep merges two objects with proper precedence.
|
|
561
|
+
*
|
|
562
|
+
* @param target Target object (lower precedence)
|
|
563
|
+
* @param source Source object (higher precedence)
|
|
564
|
+
* @returns Merged object
|
|
565
|
+
*/ function deepMergeTwo(target, source) {
|
|
566
|
+
// Handle null/undefined
|
|
567
|
+
if (source == null) return target;
|
|
568
|
+
if (target == null) return source;
|
|
569
|
+
// Handle non-objects (primitives, arrays, functions, etc.)
|
|
570
|
+
if (typeof source !== 'object' || typeof target !== 'object') {
|
|
571
|
+
return source; // Source takes precedence
|
|
572
|
+
}
|
|
573
|
+
// Handle arrays - replace entirely, don't merge
|
|
574
|
+
if (Array.isArray(source)) {
|
|
575
|
+
return [
|
|
576
|
+
...source
|
|
577
|
+
];
|
|
578
|
+
}
|
|
579
|
+
if (Array.isArray(target)) {
|
|
580
|
+
return source; // Source object replaces target array
|
|
581
|
+
}
|
|
582
|
+
// Deep merge objects
|
|
583
|
+
const result = {
|
|
584
|
+
...target
|
|
585
|
+
};
|
|
586
|
+
for(const key in source){
|
|
587
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
588
|
+
if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
|
|
589
|
+
// Recursively merge nested objects
|
|
590
|
+
result[key] = deepMergeTwo(result[key], source[key]);
|
|
591
|
+
} else {
|
|
592
|
+
// Replace with source value (higher precedence)
|
|
593
|
+
result[key] = source[key];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Loads configurations from multiple directories and merges them with proper precedence.
|
|
601
|
+
*
|
|
602
|
+
* This is the main function for hierarchical configuration loading. It:
|
|
603
|
+
* 1. Discovers configuration directories up the directory tree
|
|
604
|
+
* 2. Loads configuration files from each discovered directory
|
|
605
|
+
* 3. Merges them with proper precedence (closer directories win)
|
|
606
|
+
* 4. Returns the merged configuration with metadata
|
|
607
|
+
*
|
|
608
|
+
* @param options Configuration options for hierarchical loading
|
|
609
|
+
* @returns Promise resolving to hierarchical configuration result
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* const result = await loadHierarchicalConfig({
|
|
614
|
+
* configDirName: '.kodrdriv',
|
|
615
|
+
* configFileName: 'config.yaml',
|
|
616
|
+
* startingDir: '/project/subdir',
|
|
617
|
+
* maxLevels: 5
|
|
618
|
+
* });
|
|
619
|
+
*
|
|
620
|
+
* // result.config contains merged configuration
|
|
621
|
+
* // result.discoveredDirs shows where configs were found
|
|
622
|
+
* // result.errors contains any non-fatal errors
|
|
623
|
+
* ```
|
|
624
|
+
*/ async function loadHierarchicalConfig(options) {
|
|
625
|
+
const { configFileName, encoding = 'utf8', logger } = options;
|
|
626
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('Starting hierarchical configuration loading');
|
|
627
|
+
// Discover all configuration directories
|
|
628
|
+
const discoveredDirs = await discoverConfigDirectories(options);
|
|
629
|
+
if (discoveredDirs.length === 0) {
|
|
630
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('No configuration directories found');
|
|
631
|
+
return {
|
|
632
|
+
config: {},
|
|
633
|
+
discoveredDirs: [],
|
|
634
|
+
errors: []
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// Load configurations from each directory
|
|
638
|
+
const configs = [];
|
|
639
|
+
const errors = [];
|
|
640
|
+
// Sort by level (highest level first = lowest precedence first)
|
|
641
|
+
const sortedDirs = [
|
|
642
|
+
...discoveredDirs
|
|
643
|
+
].sort((a, b)=>b.level - a.level);
|
|
644
|
+
for (const dir of sortedDirs){
|
|
645
|
+
try {
|
|
646
|
+
const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger);
|
|
647
|
+
if (config !== null) {
|
|
648
|
+
configs.push(config);
|
|
649
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
|
|
650
|
+
} else {
|
|
651
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`No valid config found at level ${dir.level}: ${dir.path}`);
|
|
652
|
+
}
|
|
653
|
+
} catch (error) {
|
|
654
|
+
const errorMsg = `Failed to load config from ${dir.path}: ${error.message}`;
|
|
655
|
+
errors.push(errorMsg);
|
|
656
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Merge all configurations with proper precedence
|
|
660
|
+
const mergedConfig = deepMergeConfigs(configs);
|
|
661
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`Hierarchical loading complete. Merged ${configs.length} configurations`);
|
|
662
|
+
return {
|
|
663
|
+
config: mergedConfig,
|
|
664
|
+
discoveredDirs,
|
|
665
|
+
errors
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
418
669
|
/**
|
|
419
670
|
* Removes undefined values from an object to create a clean configuration.
|
|
420
671
|
* This is used to merge configuration sources while avoiding undefined pollution.
|
|
@@ -509,15 +760,70 @@ const create$1 = (params)=>{
|
|
|
509
760
|
*/ const read = async (args, options)=>{
|
|
510
761
|
var _options_defaults;
|
|
511
762
|
const logger = options.logger;
|
|
512
|
-
const storage = create$1({
|
|
513
|
-
log: logger.debug
|
|
514
|
-
});
|
|
515
763
|
const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
|
|
516
764
|
if (!rawConfigDir) {
|
|
517
765
|
throw new Error('Configuration directory must be specified');
|
|
518
766
|
}
|
|
519
767
|
const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
|
|
520
768
|
logger.debug('Resolved config directory');
|
|
769
|
+
let rawFileConfig = {};
|
|
770
|
+
// Check if hierarchical configuration discovery is enabled
|
|
771
|
+
if (options.features.includes('hierarchical')) {
|
|
772
|
+
logger.debug('Hierarchical configuration discovery enabled');
|
|
773
|
+
try {
|
|
774
|
+
// Extract the config directory name from the path for hierarchical discovery
|
|
775
|
+
const configDirName = path__namespace.basename(resolvedConfigDir);
|
|
776
|
+
const startingDir = path__namespace.dirname(resolvedConfigDir);
|
|
777
|
+
logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
|
|
778
|
+
const hierarchicalResult = await loadHierarchicalConfig({
|
|
779
|
+
configDirName,
|
|
780
|
+
configFileName: options.defaults.configFile,
|
|
781
|
+
startingDir,
|
|
782
|
+
encoding: options.defaults.encoding,
|
|
783
|
+
logger
|
|
784
|
+
});
|
|
785
|
+
rawFileConfig = hierarchicalResult.config;
|
|
786
|
+
if (hierarchicalResult.discoveredDirs.length > 0) {
|
|
787
|
+
logger.debug(`Hierarchical discovery found ${hierarchicalResult.discoveredDirs.length} configuration directories`);
|
|
788
|
+
hierarchicalResult.discoveredDirs.forEach((dir)=>{
|
|
789
|
+
logger.debug(` Level ${dir.level}: ${dir.path}`);
|
|
790
|
+
});
|
|
791
|
+
} else {
|
|
792
|
+
logger.debug('No configuration directories found in hierarchy');
|
|
793
|
+
}
|
|
794
|
+
if (hierarchicalResult.errors.length > 0) {
|
|
795
|
+
hierarchicalResult.errors.forEach((error)=>logger.warn(`Hierarchical config warning: ${error}`));
|
|
796
|
+
}
|
|
797
|
+
} catch (error) {
|
|
798
|
+
logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
|
|
799
|
+
// Fall back to single directory mode
|
|
800
|
+
logger.debug('Falling back to single directory configuration loading');
|
|
801
|
+
rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
// Use traditional single directory configuration loading
|
|
805
|
+
logger.debug('Using single directory configuration loading');
|
|
806
|
+
rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
|
|
807
|
+
}
|
|
808
|
+
const config = clean({
|
|
809
|
+
...rawFileConfig,
|
|
810
|
+
...{
|
|
811
|
+
configDirectory: resolvedConfigDir
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
return config;
|
|
815
|
+
};
|
|
816
|
+
/**
|
|
817
|
+
* Loads configuration from a single directory (traditional mode).
|
|
818
|
+
*
|
|
819
|
+
* @param resolvedConfigDir - The resolved configuration directory path
|
|
820
|
+
* @param options - Cardigantime options
|
|
821
|
+
* @param logger - Logger instance
|
|
822
|
+
* @returns Promise resolving to the configuration object
|
|
823
|
+
*/ async function loadSingleDirectoryConfig(resolvedConfigDir, options, logger) {
|
|
824
|
+
const storage = create$1({
|
|
825
|
+
log: logger.debug
|
|
826
|
+
});
|
|
521
827
|
const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);
|
|
522
828
|
logger.debug('Attempting to load config file for cardigantime');
|
|
523
829
|
let rawFileConfig = {};
|
|
@@ -539,14 +845,8 @@ const create$1 = (params)=>{
|
|
|
539
845
|
logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
|
|
540
846
|
}
|
|
541
847
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
...{
|
|
545
|
-
configDirectory: resolvedConfigDir
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
return config;
|
|
549
|
-
};
|
|
848
|
+
return rawFileConfig;
|
|
849
|
+
}
|
|
550
850
|
|
|
551
851
|
/**
|
|
552
852
|
* Error thrown when configuration validation fails
|