66/**
77 * Configuration file discovery, loading, validation, and context resolution.
88 *
9- * Pipeline:
9+ * Pipeline (lazy validation) :
1010 * 1. Discover config file in home directory (or via --config-file / ELASTIC_CLI_CONFIG_FILE)
11- * 2. Resolve expressions in config values (e.g. $(env:VAR), $(cmd:...), $(keychain:...))
12- * 3. Validate resolved config with Zod schemas
13- * 4. Resolve the active context (default or --use-context override)
14- * 5. Return typed ResolvedConfig to command handlers
11+ * 2. Structural validation of the outer config shape (without resolving expressions)
12+ * 3. Resolve context name (default or --use-context override)
13+ * 4. Resolve expressions only in the active context and commands section
14+ * 5. Validate active context with full Zod schemas
15+ * 6. Return typed ResolvedConfig to command handlers
16+ *
17+ * Only the active context's expressions are resolved, so inactive contexts
18+ * with unset environment variables or missing files will not cause failures.
1519 *
1620 * Supports:
1721 * - Home-directory discovery (~/.elasticrc.yml and variants)
@@ -26,7 +30,7 @@ import { homedir } from 'node:os'
2630import { extname , join } from 'node:path'
2731import { z } from 'zod'
2832import { parse as parseYaml } from 'yaml'
29- import { ConfigFileSchema } from './schema.ts'
33+ import { ContextSchema , CommandPolicySchema , StructuralConfigSchema } from './schema.ts'
3034import { resolveExpressions } from './resolvers.ts'
3135import type { ConfigFile , ResolvedConfig , ResolvedContext } from './types.ts'
3236
@@ -128,14 +132,19 @@ export interface LoadConfigErr { ok: false, error: { message: string } }
128132export type LoadConfigResult = LoadConfigOk | LoadConfigErr
129133
130134/**
131- * Full config loading pipeline: discover/load → validate → resolve context .
135+ * Full config loading pipeline with lazy expression resolution .
132136 *
133137 * Steps:
134138 * 1. Discover or resolve config file path, then read and parse it
135- * 2. Resolve expressions in string values (env, cmd, keychain)
136- * 3. Validate with `ConfigFileSchema` (Zod)
137- * 4. Resolve the active context (from `contextName` override or `current_context`)
138- * 5. Return a typed `ResolvedConfig`
139+ * 2. Structural validation (shape only, no expression resolution)
140+ * 3. Resolve context name (from `contextName` override or `current_context`)
141+ * 4. Extract active context raw data + commands section
142+ * 5. Resolve expressions only in the active context and commands
143+ * 6. Validate active context with ContextSchema, commands with CommandPolicySchema
144+ * 7. Return a typed `ResolvedConfig`
145+ *
146+ * Only the active context's expressions are resolved, so inactive contexts
147+ * with unset environment variables or missing files will not cause failures.
139148 *
140149 * All failure modes return `{ ok: false, error: { message } }` -- never throw.
141150 *
@@ -172,27 +181,19 @@ export async function loadConfig (options: LoadConfigOptions = {}): Promise<Load
172181 return { ok : false , error : { message } }
173182 }
174183
175- // Step 2: resolve expressions in string values
176- try {
177- raw = await resolveExpressions ( raw )
178- } catch ( err ) {
179- const message = err instanceof Error ? err . message : String ( err )
180- return { ok : false , error : { message : `Failed to resolve config expressions: ${ message } ` } }
181- }
182-
183- // Step 3: validate with Zod
184- const parsed = ConfigFileSchema . safeParse ( raw )
185- if ( ! parsed . success ) {
186- return { ok : false , error : { message : z . prettifyError ( parsed . error ) } }
184+ // Step 2: structural validation (shape only, no deep context validation)
185+ const structural = StructuralConfigSchema . safeParse ( raw )
186+ if ( ! structural . success ) {
187+ return { ok : false , error : { message : z . prettifyError ( structural . error ) } }
187188 }
188189
189- const config = parsed . data
190+ const { current_context , contexts , commands : rawCommands } = structural . data
190191
191- // Step 4 : resolve context name (--use-context override or current_context from file)
192- const resolvedContextName = contextName ?? config . current_context
192+ // Step 3 : resolve context name (--use-context override or current_context from file)
193+ const resolvedContextName = contextName ?? current_context
193194
194- if ( ! ( resolvedContextName in config . contexts ) ) {
195- const available = Object . keys ( config . contexts ) . join ( ', ' )
195+ if ( ! ( resolvedContextName in contexts ) ) {
196+ const available = Object . keys ( contexts ) . join ( ', ' )
196197 return {
197198 ok : false ,
198199 error : {
@@ -201,6 +202,41 @@ export async function loadConfig (options: LoadConfigOptions = {}): Promise<Load
201202 }
202203 }
203204
204- // Step 5: resolve and return
205- return { ok : true , value : resolveContext ( config , resolvedContextName ) }
205+ // Step 4: resolve expressions only in active context and commands (in parallel)
206+ let resolvedRawContext : unknown
207+ let resolvedRawCommands : unknown
208+ try {
209+ [ resolvedRawContext , resolvedRawCommands ] = await Promise . all ( [
210+ resolveExpressions ( contexts [ resolvedContextName ] , `contexts.${ resolvedContextName } ` ) ,
211+ rawCommands != null ? resolveExpressions ( rawCommands , 'commands' ) : undefined ,
212+ ] )
213+ } catch ( err ) {
214+ const message = err instanceof Error ? err . message : String ( err )
215+ return { ok : false , error : { message : `Failed to resolve config expressions: ${ message } ` } }
216+ }
217+
218+ // Step 5: validate active context and commands with full schemas
219+ const contextParsed = ContextSchema . safeParse ( resolvedRawContext )
220+ if ( ! contextParsed . success ) {
221+ return { ok : false , error : { message : z . prettifyError ( contextParsed . error ) } }
222+ }
223+
224+ let commands : ConfigFile [ 'commands' ]
225+ if ( resolvedRawCommands != null ) {
226+ const commandsParsed = CommandPolicySchema . safeParse ( resolvedRawCommands )
227+ if ( ! commandsParsed . success ) {
228+ return { ok : false , error : { message : z . prettifyError ( commandsParsed . error ) } }
229+ }
230+ commands = commandsParsed . data
231+ }
232+
233+ // Step 6: build and return ResolvedConfig
234+ const ctx = contextParsed . data
235+ const resolved : ResolvedContext = { }
236+ if ( ctx . elasticsearch != null ) resolved . elasticsearch = ctx . elasticsearch
237+ if ( ctx . kibana != null ) resolved . kibana = ctx . kibana
238+ if ( ctx . cloud != null ) resolved . cloud = ctx . cloud
239+ const result : ResolvedConfig = { context : resolved }
240+ if ( commands != null ) result . commands = commands
241+ return { ok : true , value : result }
206242}
0 commit comments