CLI for interacting with Elasticsearch, Elastic Cloud, and Elasticsearch Serverless control plane APIs. Targets LLM-powered agents as first-class users.
- Runtime: Node.js with native TypeScript (
--experimental-strip-types) - CLI Framework: Commander.js
- Validation: Zod v4
- Config Management: cosmiconfig (YAML serialization)
- Testing: Node.js built-in test runner (
node:test) - Linting: ESLint + TypeScript ESLint, MegaLinter (CI + pre-commit)
- TypeScript: Strict mode, ESNext + nodenext module resolution
Avoid adding new third-party dependencies to reduce supply-chain attack surface.
Commands are defined via shared config structures (see factory.ts). Custom logic is only permitted for behaviors that cannot be expressed in config.
Read ARCHITECTURE.md before proposing structural changes. It documents performance-critical design decisions (factory core split, per-endpoint API files, lazy loading, manifest format) that must not be refactored unless benchmarks prove the change is neutral.
All requirements below are non-negotiable and enforced at review time.
- JSON via stdin and
--input-file: Structured input MUST be accepted from stdin or--input-file <path>. Neither takes precedence; providing both MUST error. - CLI flags for all input fields: Every top-level schema field MUST have a corresponding kebab-case CLI flag. When both JSON input and flags are provided, flags take precedence.
- Zod input schema: Every command with structured input MUST declare a Zod schema as the single source of truth for validation, type inference, and help text.
input: true(untyped) MUST NOT be used in new commands. - Validate before executing: All input MUST be validated before any handler logic or network call. Invalid input is a hard error.
- Reject unknown keys: Input with undefined keys MUST produce a validation error naming the unknown field(s). Silent stripping is not acceptable.
--json: Every command MUST emit structured JSON when--jsonis passed.--help --json: MUST output the full JSON Schema so agents can introspect valid inputs.- Errors: All errors MUST go to stderr with a non-zero exit code. With
--json, errors MUST serialize as{"error": {"code": "...", "message": "..."}}.
--dry-run: Every command that mutates state or makes a network call MUST support--dry-run. In dry-run mode: validate all inputs, print the resolved request payload, exit 0 without executing.
- No credentials as CLI flags: API keys, passwords, and tokens belong only in the config file or environment variables. Never as CLI flags.
- Named contexts: Connection info is managed via named contexts in the YAML config (kubectl-style).
--context <name>MAY override for a single invocation; context fields MUST NOT be duplicated as first-class flags.
- Hide routing metadata:
found_in: path | query | bodyis an implementation detail. It MUST NOT appear in help text, schema output, or error messages. - Validate path parameter coverage: If a schema field has
found_in: "path"but the URL template has no matching placeholder, the system MUST fail fast at registration time.
- Paths: Use
path.join()/path.resolve(). Hard-coded/or\\separators are forbidden. - Config directories: Resolve using
os.homedir()andprocess.env.APPDATA(or OS equivalents). No Unix-only hard-coded paths. - Platform guards: Signal handling, TTY detection, and ANSI escape codes MUST be guarded behind capability checks.
- CI: The full test suite MUST pass on Windows, Linux, and macOS before merge.
All code files MUST start with:
/*
* Copyright Elasticsearch B.V. and contributors
* SPDX-License-Identifier: Apache-2.0
*/
Note the single-asterisk /* opener — scripts/check-spdx rejects the JSDoc-style /** form.
- Docstrings: All exported symbols in reusable utilities MUST have complete doc comments.
- Comments: Explain WHY, not WHAT. Do not restate code in prose.
- Naming: camelCase for functions/variables, PascalCase for types/interfaces.
- YAML config keys: Always
snake_case(e.g.api_key,current_context). Never camelCase or kebab-case. - Files: Trailing newline required, no trailing whitespace.
When solving a problem outside this tool's core domain (Elastic API interaction), check if an installed dependency solves it before writing custom code. For example: prefer commander over process.argv for argument parsing. Apply a manual solution only if the dependency cannot help.
Strict flags: noUncheckedIndexedAccess, exactOptionalPropertyTypes, strict, verbatimModuleSyntax, isolatedModules, noUncheckedSideEffectImports, moduleDetection: force. Source maps and declaration maps enabled.
Follow this cycle autonomously for every change:
- Write a failing test (RED)
- Confirm it fails for the right reason
- Write minimum code to pass (GREEN)
- Refactor under green (REFACTOR)
Task is complete when npm test exits 0 and lint passes.
MegaLinter runs automatically on staged files when pre-commit install has been run. It catches TypeScript lint errors, YAML issues, unpinned GitHub Actions, accidental secrets, and copy-paste duplication. Docker must be running.
If the pre-commit hook fails, read the error output, fix flagged files, and retry. Do NOT use --no-verify.
To run MegaLinter manually: npm run test:megalinter (requires Docker).
After every implementation change:
- Run
npm test. Never skip. - Write a regression test for every bug fix.
- Test adversarial inputs for any function embedding user strings in URLs, paths, or queries (
../,?#, empty strings, special characters). - Assert on full
RequestInitconfiguration (e.g.redirect,credentials), not just response output. - Do not copy test patterns from existing code without auditing for gaps.
- After unit tests pass, build (
npm run build) and verify end-to-end manually. - Check for lint errors on all modified files.
- Never weaken assertions, skip cases, or change expected values to get green. The test is the spec; fix the code.
- Never add special cases or dead code in production code solely to satisfy a bad test. Fix the test instead.
- Test all code paths: missing input, empty input, null, wrong types, boundary values, HTTP error codes, redirects, timeouts.
- Run the code you wrote. For a CLI command, run it. For a request builder, trace the actual HTTP request.
When constructing URLs, sending credentials, or making HTTP requests:
- Encode path parameters: Use
encodeURIComponent()or an equivalent wrapper. Never bareString(value). - Validate URL schemes: Reject anything other than
http://orhttps://. Warn on plaintexthttp://for non-localhost targets. - Audit before copying: If replicating URL-building logic from another file, check it for encoding and validation gaps.
- Set
redirect: 'error'(or'manual') on everyfetchcall that sends credentials. Never rely on the default'follow'. - Assert on
RequestInitin tests: verifyredirect,method, andheaders. - Adversarial input test: Add at least one test with malicious input for any function that embeds user strings in URLs or paths.
-
Enumerate all variants upfront.
unwrapField()only handledoptionalanddefault; codegen also producedz.lazy(),z.record(),z.any(),z.union(), which all fell through silently. Inspect the full set of types the upstream system can produce and add explicit branches or a loud failure for unrecognized cases. -
Fail loudly on unrecognized input. A catch-all
return { typeName: def.type, isOptional: false }silently returned garbage.throw new Error('unhandled Zod type: ' + def.type)would have surfaced the problem at registration time. -
Test with real generated schemas. Hand-crafted toy schemas miss codegen-specific types like
z.lazy. -
Generic request builders need extension points for endpoint-specific semantics. The bulk API needs NDJSON; the index API needs body promotion. Add explicit extension points (
bodyFormat,BODY_ROOT_FIELDS) rather than special-casing later. -
Diagnose common mistakes in user-facing errors. Map known error patterns (TLS mismatch, auth failure, DNS) to actionable hints instead of propagating raw messages.
-
Treat codegen output as untrusted input. Validate assumptions about which Zod types appear with tests that use actual generated schemas.
-
Read external type definitions before setting properties.
TransportRequestParamshasbulkBodyfor NDJSON, notbody+ custom headers. JavaScript silently ignores extra properties; onlytsc --noEmitor CI catches this. Runnpx tsc --noEmitbefore pushing. -
Guard clauses that discard data are dangerous.
if (!(def.body instanceof z.ZodObject)) return undefinedsilently dropped all stdin/--input-fileinput for Cloud POST commands. When a guard returns early with no data, ask what happens to the caller's input. Prefer forwarding with passthrough semantics over silent discard. -
Trace the full data flow for every mode combination.
--jsonbroke cat APIs because the handler returned raw text and the factory blindly calledJSON.stringify(). When two layers cooperate (handler + formatter, request builder + transport), enumerate all mode combinations and verify each. -
Review codegen command names for UX. Machine-generated names (e.g.
list-deployments) are precise but verbose. Add short aliases where unambiguous so users can discover commands intuitively.
Uses spec-kit for AI-assisted feature development.
| Path | Purpose |
|---|---|
.specify/specs/ |
Feature specifications |
.specify/plans/ |
Implementation plans |
.specify/tasks/ |
Task definitions |
.specify/memory/ |
Long-lived context (e.g. constitution.md) |
.specify/templates/ |
Markdown templates |
.specify/scripts/ |
Helper scripts |
.specify/hooks.yml |
CI/automation hooks |
All commit messages and PR titles MUST follow Conventional Commits v1.0.0. PR titles are validated in CI.
<type>(<optional scope>)<optional !>: <description>
<optional body>
<optional footers>
| Type | When to use | Triggers release? |
|---|---|---|
feat |
New user-facing feature | Yes (minor) |
fix |
Bug fix | Yes (patch) |
perf |
Performance improvement, no API change | Yes (patch) |
docs |
Documentation only | No |
test |
Adding or updating tests | No |
ci |
CI/CD changes | No |
chore |
Dependencies, tooling, maintenance | No |
refactor |
Code restructuring, no behavior change | No |
revert |
Reverts a previous commit | Depends |
Use a scope when the change is confined to one area. Omit for cross-cutting changes.
Common scopes: cli, config, auth, cloud, es, serverless.
feat(cli): add --format flag to search command
fix(config): handle missing YAML key gracefully
ci: add PR title validation step
Indicate with ! before the colon, a BREAKING CHANGE footer, or both:
feat(cli)!: rename --output to --format
BREAKING CHANGE: --output is removed; use --format instead.
release-please automates versioning from commit messages via squash-merge.
To override a merged commit message, add to the PR body:
BEGIN_COMMIT_OVERRIDE
feat(cli): correct description
fix(config): secondary fix
END_COMMIT_OVERRIDE
To force a specific version, use the Release-As trailer:
chore: release 3.0.0
Release-As: 3.0.0
- Types and scopes must be lowercase:
feat, notFeat;feat(cli), notfeat(CLI). - No space before the colon:
feat:, notfeat :. - Space after the colon:
feat: add, notfeat:add. - Description must not be capitalized or end with a period.
- Empty scope parentheses are invalid: omit them entirely.
- Every commit must have a type prefix.