Description
For most CLI invocations, the configuration file is loaded, expressions are resolved, and schemas are validated twice:
- Early load (line ~97):
loadConfig({}) is called unconditionally (for all commands except version) to populate the config store so hideBlockedCommands can filter the help text.
preAction hook (line ~31): loadConfig() is called again with the same options right before the command handler runs.
Both calls perform the full pipeline: file discovery, YAML parsing, structural validation, expression resolution, and context schema validation. The setResolvedConfig call in the early load is overwritten by the preAction hook, so the first load's result is discarded (except for its use by hideBlockedCommands).
For configs that use slow expression resolvers (e.g., $(keychain:...), $(secret_service:...), $(credential_manager:...), $(cmd:...)), this doubles the number of shell-outs per invocation, adding noticeable latency.
Steps to Reproduce
- Create a
.elasticrc.yml that uses a slow resolver, e.g.:
current_context: default
contexts:
default:
elasticsearch:
url: https://localhost:9200
auth:
api_key: $(keychain:elastic-cli/api-key)
- Run any command and observe that the keychain lookup happens twice:
- Alternatively, use
$(cmd:sleep 1) as a synthetic slow resolver to make the double-load obvious:
current_context: default
contexts:
default:
elasticsearch:
url: https://localhost:9200
auth:
api_key: $(cmd:sleep 1 && echo my-key)
elastic es info will take ~2 seconds instead of ~1.
Expected behavior
The config should be loaded and resolved once per invocation. The result should be reused by both hideBlockedCommands and the preAction hook.
Root Cause
In src/cli.ts, the early load on line ~97 does not pass --config-file or --use-context options (it calls loadConfig({}) with no arguments), while the preAction hook on line ~31 passes the CLI flags. Since they may resolve different configs, the early load's result cannot simply be reused by the preAction hook as-is.
However, in practice the early load's result is always discarded by the preAction hook's setResolvedConfig call, making the early load purely overhead except for the hideBlockedCommands side effect.
Recommended Solution
Cache the early load result and reuse it in the preAction hook when the CLI flags match (i.e., no --config-file or --use-context overrides). For example:
let earlyConfig: ResolvedConfig | undefined
if (firstArg !== 'version') {
const earlyResult = await loadConfig({})
if (earlyResult.ok) {
earlyConfig = earlyResult.value
setResolvedConfig(earlyResult.value)
hideBlockedCommands(program, earlyResult.value.commands)
}
}
program.hook('preAction', async (thisCommand, actionCommand) => {
if (actionCommand.name() === 'version') return
if (actionCommand.parent?.name() === 'docs') return
const { configFile: configPath, useContext: contextName } = thisCommand.opts()
// Reuse the early load if no overrides were specified
if (configPath == null && contextName == null && earlyConfig != null) {
setResolvedConfig(earlyConfig)
return
}
const result = await loadConfig({
...(configPath != null && { configPath }),
...(contextName != null && { contextName })
})
if (result.ok) {
setResolvedConfig(result.value)
} else {
process.stderr.write(`Error: ${result.error.message}\n`)
process.exit(1)
}
})
This eliminates the second load for the common case (no CLI overrides) while preserving correct behavior when --config-file or --use-context are specified.
Related
Parent issue: #71
Description
For most CLI invocations, the configuration file is loaded, expressions are resolved, and schemas are validated twice:
loadConfig({})is called unconditionally (for all commands exceptversion) to populate the config store sohideBlockedCommandscan filter the help text.preActionhook (line ~31):loadConfig()is called again with the same options right before the command handler runs.Both calls perform the full pipeline: file discovery, YAML parsing, structural validation, expression resolution, and context schema validation. The
setResolvedConfigcall in the early load is overwritten by thepreActionhook, so the first load's result is discarded (except for its use byhideBlockedCommands).For configs that use slow expression resolvers (e.g.,
$(keychain:...),$(secret_service:...),$(credential_manager:...),$(cmd:...)), this doubles the number of shell-outs per invocation, adding noticeable latency.Steps to Reproduce
.elasticrc.ymlthat uses a slow resolver, e.g.:time elastic es info$(cmd:sleep 1)as a synthetic slow resolver to make the double-load obvious:elastic es infowill take ~2 seconds instead of ~1.Expected behavior
The config should be loaded and resolved once per invocation. The result should be reused by both
hideBlockedCommandsand thepreActionhook.Root Cause
In
src/cli.ts, the early load on line ~97 does not pass--config-fileor--use-contextoptions (it callsloadConfig({})with no arguments), while thepreActionhook on line ~31 passes the CLI flags. Since they may resolve different configs, the early load's result cannot simply be reused by thepreActionhook as-is.However, in practice the early load's result is always discarded by the
preActionhook'ssetResolvedConfigcall, making the early load purely overhead except for thehideBlockedCommandsside effect.Recommended Solution
Cache the early load result and reuse it in the
preActionhook when the CLI flags match (i.e., no--config-fileor--use-contextoverrides). For example:This eliminates the second load for the common case (no CLI overrides) while preserving correct behavior when
--config-fileor--use-contextare specified.Related
Parent issue: #71