Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a4eee36
feat(extension): add extension registry store module
margaretjgu May 12, 2026
51f8bba
test(extension): add unit tests for extension store
margaretjgu May 12, 2026
033b998
fix(extension): validate registry schema, restrict permissions, rejec…
margaretjgu May 12, 2026
6cd53a6
fix(extension): skip chmod on win32 to fix Windows CI permissions test
margaretjgu May 12, 2026
24c4ad8
fix(extension): use early return for Windows skip instead of options …
margaretjgu May 12, 2026
9d8c180
feat(extension): add installer module for github and npm sources
margaretjgu May 12, 2026
2346150
test(extension): add unit tests for installer module
margaretjgu May 12, 2026
85bb3eb
feat(extension): add elastic extension command group (list, install, …
margaretjgu May 12, 2026
9958a96
feat(extension): wire extension command group and skip config loading
margaretjgu May 12, 2026
c262905
feat(extension): add context env-var builder for extensions
margaretjgu May 12, 2026
98ea76a
docs(extension): document credential trust model in context.ts
margaretjgu May 12, 2026
2d1f5e9
feat(extension): add extension runner module
margaretjgu May 12, 2026
9e829f6
feat(extension): dispatch unknown top-level commands to installed ext…
margaretjgu May 12, 2026
5138553
feat(extension): add upgradeExtension and upgradeAllExtensions
margaretjgu May 12, 2026
9270cee
feat(extension): add searchExtensions via GitHub topic API
margaretjgu May 12, 2026
5c70ffa
feat(extension): wire upgrade and search commands into extension group
margaretjgu May 12, 2026
1f6af37
test(extension): add tests for upgrade and search
margaretjgu May 12, 2026
4d7d206
fix(extension): remove unused params from fetch stub to satisfy eslint
margaretjgu May 12, 2026
e0f1fae
feat(extension): add local: source type for development and e2e testing
margaretjgu May 13, 2026
31784bc
Revert "feat(extension): add local: source type for development and e…
margaretjgu May 13, 2026
bf167e4
Merge remote-tracking branch 'origin/feat/extension-search-upgrade' i…
margaretjgu May 13, 2026
ac44a93
fix(extension): return structured errors instead of throwing in comma…
margaretjgu May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
for (let c = actionCommand.parent; c != null; c = c.parent) {
if (c.name() === 'config') return
}
// `extension` commands manage the extension registry, not the Elastic stack
for (let c = actionCommand.parent; c != null; c = c.parent) {
if (c.name() === 'extension') return
}
const { configFile: configPath, useContext: contextName, commandProfile: profileName } = thisCommand.opts()
const typedProfileName = profileName as BuiltInProfile | undefined

Expand Down Expand Up @@ -176,11 +180,18 @@ if (firstArg === 'sanitize') {
program.addCommand(defineGroup({ name: 'sanitize', description: 'Sanitize values for safe use in Elasticsearch' }))
}

if (firstArg === 'extension') {
const { registerExtensionCommands } = await import('./extension/register.ts')
program.addCommand(registerExtensionCommands())
} else {
program.addCommand(defineGroup({ name: 'extension', description: 'Manage elastic CLI extensions' }))
}

// Load config early so --help can hide blocked commands. Skip for commands
// that don't need config (e.g. `version`, `sanitize`, or `config` which authors the file)
// to avoid unnecessary file I/O and a confusing "no config found" path.
// The result is cached in earlyConfig so the preAction hook can reuse it.
if (firstArg !== 'version' && firstArg !== 'config' && firstArg !== 'sanitize') {
if (firstArg !== 'version' && firstArg !== 'config' && firstArg !== 'sanitize' && firstArg !== 'extension') {
// Parse --profile early (before Commander's full parse) so the early config load
// and hideBlockedCommands can apply the correct profile-based allow-list to --help.
const profileArgIdx = process.argv.indexOf('--command-profile')
Expand All @@ -203,4 +214,24 @@ if (process.argv.slice(2).length === 0) {
process.exit(0)
}

// If the first argument does not match any built-in command, attempt to
// dispatch to an installed extension named `elastic-<firstArg>`.
// This check runs after all built-ins are registered so the set is complete.
const BUILT_IN_COMMANDS = new Set([
'version', 'stack', 'es', 'elasticsearch', 'kb', 'kibana',
'cloud', 'docs', 'config', 'sanitize', 'extension',
])

if (firstArg != null && !BUILT_IN_COMMANDS.has(firstArg)) {
const { findExtension } = await import('./extension/store.ts')
const ext = await findExtension(firstArg)
if (ext != null) {
const { buildContextEnv } = await import('./extension/context.ts')
const { runExtension } = await import('./extension/runner.ts')
const contextEnv = earlyConfig?.ok === true ? buildContextEnv(earlyConfig.value) : {}
const exitCode = await runExtension(ext, process.argv.slice(3), contextEnv)
process.exit(exitCode)
}
}

await program.parseAsync(process.argv)
66 changes: 66 additions & 0 deletions src/extension/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Converts a resolved CLI config into a flat set of environment variables
* that extensions can read to connect to Elastic services without re-implementing
* config parsing.
*
* Only variables with a defined value are included in the returned object.
* Extensions must treat absent variables as "service not configured".
*
* Security / trust model:
* - Credentials are passed as env vars, which is the standard approach for
* CLI extension systems (same model as `gh`). On Linux, a process's env is
* readable by root via /proc/<pid>/environ and by any process running as the
* same user, so this offers no additional protection over the config file.
* - All child processes spawned by the extension inherit these env vars. Authors
* should be aware and avoid leaking them into further subprocesses or logs.
* - The caller (runner) must NOT use `shell: true` when spawning extensions.
* Use spawn with an explicit args array to avoid shell injection.
*
* Exported variable names:
* ELASTIC_ES_URL Elasticsearch URL
* ELASTIC_ES_API_KEY Elasticsearch API key (api_key auth)
* ELASTIC_ES_USERNAME Elasticsearch username (basic auth)
* ELASTIC_ES_PASSWORD Elasticsearch password (basic auth)
* ELASTIC_KIBANA_URL Kibana URL
* ELASTIC_KIBANA_API_KEY Kibana API key
* ELASTIC_KIBANA_USERNAME Kibana username (basic auth)
* ELASTIC_KIBANA_PASSWORD Kibana password (basic auth)
* ELASTIC_CLOUD_URL Elastic Cloud URL
* ELASTIC_CLOUD_API_KEY Elastic Cloud API key
*/

import type { ResolvedConfig, ServiceBlock } from '../config/types.ts'

type EnvMap = Record<string, string>

function serviceEnv (prefix: string, block: ServiceBlock): EnvMap {
const env: EnvMap = {}
env[`${prefix}_URL`] = block.url
if (block.auth == null) return env
if ('api_key' in block.auth) {
env[`${prefix}_API_KEY`] = block.auth.api_key
} else {
env[`${prefix}_USERNAME`] = block.auth.username
env[`${prefix}_PASSWORD`] = block.auth.password
}
return env
}

/**
* Returns a flat `Record<string, string>` of environment variables derived
* from the resolved config. Merge this into the child process `env` when
* spawning an extension.
*/
export function buildContextEnv (config: ResolvedConfig): EnvMap {
const env: EnvMap = {}
const { elasticsearch, kibana, cloud } = config.context
if (elasticsearch != null) Object.assign(env, serviceEnv('ELASTIC_ES', elasticsearch))
if (kibana != null) Object.assign(env, serviceEnv('ELASTIC_KIBANA', kibana))
if (cloud != null) Object.assign(env, serviceEnv('ELASTIC_CLOUD', cloud))
return env
}
Loading
Loading