1818import fs from "node:fs" ;
1919import os from "node:os" ;
2020import path from "node:path" ;
21- import { z } from "zod" ;
21+ import {
22+ ZodArray ,
23+ ZodDefault ,
24+ ZodError ,
25+ ZodObject ,
26+ ZodOptional ,
27+ type z ,
28+ } from "zod" ;
2229import { type Config , configSchema } from "./schema.js" ;
2330
2431const CONFIG_FILE_NAMES = [
@@ -27,15 +34,27 @@ const CONFIG_FILE_NAMES = [
2734 "toolkit-md.config.json" ,
2835] ;
2936
37+ interface ConfigEntry {
38+ schema : z . ZodType ;
39+ cli ?: string ;
40+ env ?: string ;
41+ envPrefix ?: string ;
42+ isArray : boolean ;
43+ }
44+
3045export class ConfigManager {
3146 private fileConfig : Partial < Config > = { } ;
3247 private configFilePath : string | null = null ;
3348 private cliOptions : any = { } ;
49+ private entries : Map < string , ConfigEntry > ;
3450
3551 constructor (
3652 private cwd : string ,
3753 private schema = configSchema ,
38- ) { }
54+ ) {
55+ this . entries = new Map ( ) ;
56+ this . buildEntries ( this . schema , "" ) ;
57+ }
3958
4059 public getCwd ( ) {
4160 return this . cwd ;
@@ -50,10 +69,7 @@ export class ConfigManager {
5069 cliOptions : any = { } ,
5170 configPath ?: string ,
5271 ) : Promise < void > {
53- // Store CLI options for later use
5472 this . cliOptions = cliOptions ;
55-
56- // Load configuration from file(s)
5773 await this . loadFromConfigFile ( configPath ) ;
5874 }
5975
@@ -71,25 +87,18 @@ export class ConfigManager {
7187 * @throws Error if validation fails
7288 */
7389 public get < T > ( path : string , defaultOverride ?: T ) : T {
74- // Find the schema node for this path
75- const schemaNode = this . getSchemaNodeForPath ( path ) ;
90+ const entry = this . entries . get ( path ) ;
7691
77- if ( ! schemaNode ) {
92+ if ( ! entry ) {
7893 throw new Error ( `Config path ${ path } is not valid` ) ;
7994 }
8095
8196 let value : any ;
8297
83- // 1. Check CLI options (highest priority)
84- if ( schemaNode . cli && this . cliOptions [ schemaNode . cli ] !== undefined ) {
85- value = this . cliOptions [ schemaNode . cli ] ;
86- }
87- // 2. Check environment variables
88- else if ( schemaNode . env ) {
89- const envVars = Array . isArray ( schemaNode . env )
90- ? schemaNode . env
91- : [ schemaNode . env ] ;
92-
98+ if ( entry . cli && this . cliOptions [ entry . cli ] !== undefined ) {
99+ value = this . cliOptions [ entry . cli ] ;
100+ } else if ( entry . env ) {
101+ const envVars = Array . isArray ( entry . env ) ? entry . env : [ entry . env ] ;
93102 for ( const envVar of envVars ) {
94103 if ( process . env [ envVar ] !== undefined ) {
95104 value = process . env [ envVar ] ;
@@ -98,47 +107,33 @@ export class ConfigManager {
98107 }
99108 }
100109
101- // Special handling for array values from environment variables with prefix
102- if (
103- value === undefined &&
104- schemaNode . _zod . def . innerType instanceof z . ZodArray &&
105- schemaNode . envPrefix
106- ) {
110+ if ( value === undefined && entry . isArray && entry . envPrefix ) {
107111 const values : any [ ] = [ ] ;
108-
109- // Collect all environment variables with the prefix
110112 for ( const [ key , val ] of Object . entries ( process . env ) ) {
111- if ( key . startsWith ( schemaNode . envPrefix + ( "_" as const ) ) && val ) {
113+ if ( key . startsWith ( ` ${ entry . envPrefix } _` ) && val ) {
112114 values . push ( val ) ;
113115 }
114116 }
115-
116- // If we found any values, use them
117117 if ( values . length > 0 ) {
118118 value = values ;
119119 }
120120 }
121121
122- // 3. Check config file
123122 if ( value === undefined ) {
124123 const fileValue = this . getNestedProperty < any > ( this . fileConfig , path ) ;
125124 if ( fileValue !== undefined ) {
126125 value = fileValue ;
127126 }
128127 }
129128
130- // 4. Use default value from schema or provided override
131129 if ( value === undefined ) {
132130 value = defaultOverride ;
133131 }
134132
135- // Validate and transform using Zod
136133 try {
137- // Parse the value through the schema node
138- const result = schemaNode . parse ( value ) ;
139- return result as T ;
134+ return entry . schema . parse ( value ) as T ;
140135 } catch ( error ) {
141- if ( error instanceof z . ZodError ) {
136+ if ( error instanceof ZodError ) {
142137 const errorMessages = error . issues . map ( ( e ) => e . message ) . join ( ", " ) ;
143138 throw new Error ( `Invalid configuration for ${ path } : ${ errorMessages } ` ) ;
144139 }
@@ -153,30 +148,37 @@ export class ConfigManager {
153148 return this . configFilePath ;
154149 }
155150
156- /**
157- * Find the schema node for a given path
158- */
159- private getSchemaNodeForPath ( path : string ) : z . ZodDefault < any > | undefined {
160- const keys = path . split ( "." ) ;
161- let current : any = this . schema ;
162-
163- for ( const key of keys ) {
164- if ( ! current ) return undefined ;
165-
166- // Handle object schemas
167- if ( current instanceof z . ZodObject ) {
168- const shape = current . shape ;
169- current = shape [ key ] ;
170- }
171- // Handle array schemas
172- else if ( current instanceof z . ZodArray && ! Number . isNaN ( key ) ) {
173- current = current . element ;
174- } else {
175- return undefined ;
151+ private buildEntries ( schema : z . ZodType , prefix : string ) : void {
152+ if ( schema instanceof ZodObject ) {
153+ for ( const [ key , value ] of Object . entries (
154+ ( schema as ZodObject < any > ) . shape ,
155+ ) ) {
156+ const fullPath = prefix ? `${ prefix } .${ key } ` : key ;
157+ this . buildEntries ( value as z . ZodType , fullPath ) ;
176158 }
159+ return ;
177160 }
178161
179- return current ;
162+ const meta = schema . meta ( ) as z . GlobalMeta | undefined ;
163+ const isArray = this . unwrapSchema ( schema ) instanceof ZodArray ;
164+
165+ this . entries . set ( prefix , {
166+ schema,
167+ cli : meta ?. cli ,
168+ env : meta ?. env ,
169+ envPrefix : meta ?. envPrefix ,
170+ isArray,
171+ } ) ;
172+ }
173+
174+ private unwrapSchema ( schema : z . ZodType ) : z . ZodType {
175+ if ( schema instanceof ZodDefault ) {
176+ return ( schema as ZodDefault < any > ) . _zod . def . innerType as z . ZodType ;
177+ }
178+ if ( schema instanceof ZodOptional ) {
179+ return ( schema as ZodOptional < any > ) . _zod . def . innerType as z . ZodType ;
180+ }
181+ return schema ;
180182 }
181183
182184 /**
@@ -187,16 +189,12 @@ export class ConfigManager {
187189 try {
188190 let configFilePath : string | null = null ;
189191
190- // 1. Try explicit path if provided
191192 if ( explicitPath ) {
192193 if ( fs . existsSync ( explicitPath ) ) {
193194 configFilePath = explicitPath ;
194- } else {
195- // Silently continue to other config file locations
196195 }
197196 }
198197
199- // 2. Try current directory
200198 if ( ! configFilePath ) {
201199 for ( const fileName of CONFIG_FILE_NAMES ) {
202200 const filePath = path . join ( this . cwd , fileName ) ;
@@ -207,7 +205,6 @@ export class ConfigManager {
207205 }
208206 }
209207
210- // 3. Try home directory
211208 if ( ! configFilePath ) {
212209 for ( const fileName of CONFIG_FILE_NAMES ) {
213210 const filePath = path . join ( os . homedir ( ) , fileName ) ;
@@ -218,13 +215,11 @@ export class ConfigManager {
218215 }
219216 }
220217
221- // If we found a config file, load and parse it
222218 if ( configFilePath ) {
223219 const fileContent = await fs . promises . readFile ( configFilePath , "utf8" ) ;
224220 this . fileConfig = JSON . parse ( fileContent ) ;
225221 this . configFilePath = configFilePath ;
226222 } else {
227- // No config file found, use empty object
228223 this . fileConfig = { } ;
229224 }
230225 } catch ( error : any ) {
0 commit comments