Skip to content

feat(cli-docs): auto-generate CLI reference from schema JSON #3221

Draft
Mpdreamz wants to merge 18 commits intofeature/arghfrom
feature/cli-generated-docs
Draft

feat(cli-docs): auto-generate CLI reference from schema JSON #3221
Mpdreamz wants to merge 18 commits intofeature/arghfrom
feature/cli-generated-docs

Conversation

@Mpdreamz
Copy link
Copy Markdown
Member

@Mpdreamz Mpdreamz commented Apr 30, 2026

What is this?

This PR adds automatic CLI reference documentation generation to docs-builder. Point it at a JSON schema file describing a CLI's command tree and it generates a full, navigable reference — one page per command, one page per namespace — wired directly into the docs site's table of contents.

docs-builder's own CLI reference (/cli/) is now generated this way.

cursorful-video-1777577780994.mp4

Why PR #3202 is a prerequisite

This PR consumes a schema JSON file that describes a CLI's full command tree — every namespace, command, and parameter — with names, types, required/optional, descriptions, default values, and enum choices. The schema format is straightforward and any CLI from any ecosystem can emit it.

docs-builder inherits schema generation from Nullean.Argh, which is what PR #3202 migrates to. Nullean.Argh 0.13.0 introduced a built-in --emit-schema / __schema command that outputs exactly this JSON. That's the convenient on-ramp — but a Go CLI, a Python CLI, or anything else that can write the same JSON structure can use this system just as well.

Configuring it

One new key in docset.yml:

toc:
  - cli: cli/schema.json      # path to the schema JSON
    folder: cli               # virtual URL root and supplemental docs folder
    children:                 # optional: hand-written pages that appear first
      - file: installation.md
      - file: configuration.md

That's it. docs-builder discovers the full command tree from the schema and generates the rest. The children list is for narrative pages (installation guides, config walkthroughs) that sit alongside the reference.

To serve a CLI repo's docs standalone:

docs-builder serve --path /path/to/cli-repo/docs

(This PR also fixes a scoped-filesystem bug that prevented --path from pointing outside the docs-builder repo.)

What gets generated

Namespace index pages

Every namespace (elastic stack, elastic cloud hosted, …) gets an index page with:

  • A cli namespace badge right-aligned in the <h1>
  • A binary namespace --help usage codeblock
  • Clickable page cards for every sub-namespace and command — full-width bordered rows with a title, description, and right-pointing chevron
┌────────────────────────────────────────────────────────────┐
│  put-view                                                → │
│  Create or update an ES|QL view.                           │
└────────────────────────────────────────────────────────────┘

The {page-card} directive

The page cards on namespace indexes are powered by a new general-purpose {page-card} directive, available in any hand-written Markdown page:

:::{page-card} [Installation](./installation.md)
Get the CLI installed and linked in under a minute.
:::

:::{page-card} [Elasticsearch overview](elasticsearch://docs/get-started.md)
Cross-link to another docs set using the standard crosslink scheme.
:::

The argument must be a Markdown link — [Title](url). The URL is restricted to local relative paths (.md files) or crosslink scheme URIs (e.g. elasticsearch://…). Absolute http:// / https:// URLs are rejected at build time with a clear error, keeping page cards as in-docs navigation rather than external links. The body (description line) is optional.

Command pages

Every command gets a page with:

  • A cli command badge right-aligned in the <h1> (amber, matching the sidebar pill)
  • A usage line — always present, always copy-paste ready
  • Separate Arguments and Options sections rendered as definition lists

Usage lines that are actually useful

When the schema provides an explicit usage string it's used as-is (with automatic wrapping at 80 characters into multiline bash-continuation style). When there's no explicit usage — which is common — docs-builder generates one from the schema itself:

elastic stack es esql put-view --name <name> --query <query> [options]

Required named flags appear individually with their placeholder (--flag <flag>). Positional arguments are listed with angle brackets. Optional flags collapse to [options]. The result is a line you can copy, fill in the blanks, and run — not a generic [options] mystery.

Parameter anchors

Every flag and option has a fragment anchor (#flag-name). Hover any parameter term to reveal a # link; clicking it updates the URL so you can share a direct link to a specific flag.

Full-path page titles and sidebar pills

Pages carry their full command path as the title (assembler bloom-filter, not just bloom-filter) so they're unambiguous in search and browser history. The sidebar shows colour-coded pills — purple for namespaces, amber for commands — matching the badge colours in the heading.

Injecting custom content (supplemental docs)

The generated content is great for reference, but sometimes you need context, examples, or warnings that don't belong in the schema. The supplemental docs system handles this without touching the generator.

Folder layout (using folder: cli as the virtual root):

docs/cli/
├── index.md              # overrides the root CLI page body
├── installation.md       # extra hand-written page (listed in children:)
├── configuration.md      # extra hand-written page
├── assembler/
│   └── index.md          # overrides the `assembler` namespace body
└── cmd-serve.md          # overrides the `serve` command body (flat style)

Alternative flat naming also works: ns-assembler.md, cmd-assembler-bloom-filter.md.

Supplemental content replaces only the description section. The schema-driven heading, usage codeblock, parameter tables, and navigation cards still render — supplemental adds human context on top of machine precision.

Keeping docs in sync with the CLI

Generated docs are only valuable if they stay accurate. A CI step enforces this — it re-runs docs-builder __schema, diffs the output against the committed docs/cli-schema.json, and fails the build if they differ:

- name: Check CLI schema is up to date
  run: |
    dotnet run --project src/tooling/docs-builder -- __schema > docs/cli-schema.json.tmp
    diff docs/cli-schema.json docs/cli-schema.json.tmp || \
      (echo "docs/cli-schema.json is out of date — run: docs-builder __schema > docs/cli-schema.json" && exit 1)

Add a command, rename a flag, change a description — CI tells you to regenerate the schema before merging. The docs can never silently lag behind the CLI.

Test plan

  • docs-builder serve --path docs/cli/ renders docs-builder's own CLI reference
  • docs-builder serve --path <external-cli-repo>/docs → CLI reference renders with correct binary name in usage lines
  • Namespace pages show page cards that navigate correctly on click
  • Command pages without a schema usage field show a generated usage line with required flags explicit
  • Parameter terms have fragment anchors; clicking one updates the browser URL
  • {page-card} directive works in a hand-authored .md page
  • Sidebar shows elastic CLI / docs-builder CLI root titles with correct colour pills

🤖 Generated with Claude Code

Mpdreamz and others added 13 commits April 29, 2026 21:54
Made with ❤️️ by updatecli

Co-authored-by: elastic-observability-automation[bot] <180520183+elastic-observability-automation[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds a new `cli:` toc entry type in docset.yml that renders an entire
CLI reference section — namespaces, commands, parameters, usage — from
an argh __schema JSON file with no hand-maintained markdown.

Key pieces:
- `CliReferenceRef` toc record + YAML parser (cli: + folder: + children:)
- `ArghSchema` deserialization models with AOT-safe STJ source gen
- `CliRootFile`, `CliNamespaceFile`, `CliCommandFile` — MarkdownFile
  subrecords that synthesise page content from schema data
- `CliReferenceDocsBuilderExtension` — auto-enabled when a cli: entry
  is present; pre-creates synthetic files, handles supplemental docs
- Navigation builder wires synthetic files into the nav tree; explicit
  children: prepend regular docs before generated pages
- Supplemental docs: changelog/index.md and ns-/cmd- prefix conventions
  inject intro prose into generated pages; validation errors on mismatches
- Multiline bash wrapping for usage lines >80 chars
- [ns]/[cmd] nav pills (inline style, right-aligned) distinguish
  generated pages in the sidebar; breadcrumb/prev-next strip the prefix
- Fix: serve no longer crashes in git worktrees (ReloadGeneratorService
  falls back to SourceDirectory when DocumentationCheckoutDirectory is null)
- Fix: synthetic files recognised by link validator (existsInSet check)
- CI step added to ci.yml to keep docs/cli-schema.json current
- docs/cli/ replaced: hand-written pages removed, replaced with
  generated reference + installation, shell-autocompletion, changelog
  supplemental docs, and an "Automated Reference" how-to guide

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Schema v2 changes (from argh PR#39):
- entryAssembly renamed to name
- kind replaced by type using JSON Schema primitives
  ("string", "integer", "number", "boolean", "array", "enum")
- New param fields: defaultValue, enumValues, elementType, repeatable,
  separator, aliases, hidden
- New command fields: aliases, hidden
- schemaVersion bumped from 1 to 2

ArghSchema.cs updated to v2 models while keeping v1 fallback support
in FormatKindV1 for generators not yet on the new format.

CliMarkdownGenerator updates:
- IsBoolFlag and FormatTypeHint now use JSON Schema type strings
- EnumValues rendered from schema field instead of parsing summary text
- DefaultValue rendered from schema field (skips literal "default")
- Hidden params/commands filtered from generated pages

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ards

New general-purpose {page-card} directive renders a bordered full-row
navigation card with title, description, and right chevron — similar
to the prev/next navigation at the bottom of pages.

Usage:
  :::{page-card} [Title](./path/to/page.md)
  Optional description text.
  :::

Constraints:
- Argument must be a markdown link [Title](url)
- URL must be a local .md path or crosslink, not an absolute URL
- URL is resolved using the same logic as DiagnosticLinkInlineParser
  (relative to the source file's directory), so links always point to
  the correct page regardless of nesting depth

CliMarkdownGenerator updated to emit {page-card} blocks for Commands
and Sub-namespaces/Namespaces sections on namespace and root pages,
replacing the plain definition lists.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…tion attrs

- PageCardBlock.FinalizeAndValidate now resolves the relative URL using
  the same logic as DiagnosticLinkInlineParser (relative to source file
  directory, not the docroot), so ./cmd-init.md from cli/changelog/index.md
  correctly resolves to /cli/changelog/cmd-init
- PageCardView adds hx-select-oob and preload attributes so htmx partial
  page updates work the same as all other internal links

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…hor indicators

Clean URLs for CLI command pages (no cmd- prefix):
- /cli/assembler/deploy/apply instead of /cli/assembler/deploy/cmd-apply
- Exception: commands named "index" keep cmd- prefix to avoid collision
  with namespace index.md pages (e.g. assembler/index.md)
- Works for both serve (URL routing) and static HTML build (output path)
- Physical supplemental cmd-*.md files share the same CliFile instance
  as the synthetic clean path via a _createdFiles cache, ensuring
  NavigationDocumentationFileLookup finds the file from either key

Fragment anchors on parameter definitions:
- DefinitionListAnchorRenderer subclasses HtmlDefinitionListRenderer
  to inject id attributes on <dt> elements containing inline code
- id extracted from the longest --flag-name; positional terms get <name>
- Only triggers for code-quoted terms (backtick syntax)
- Registered via UseDefinitionTermAnchors() pipeline extension

Hover anchor indicators:
- Heading .headerlink shows # via ::after pseudo-element on hover
- dt[id] shows # via ::before pseudo-element on hover
- Uses CSS opacity transition, # hidden by default

Navigation title virtual property made virtual on MarkdownFile.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace CSS ::before pseudo-element with an actual <a class="paramlink">
inside each <dt id> element. Clicking # now updates the URL fragment
(e.g. /cli/assembler/deploy/apply#environment) and can be shared.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…able

Wrap the entire <dt> content in <a class="paramlink"> (same pattern as
headings), so clicking anywhere on the term navigates to the fragment.
# appears to the left via CSS ::before on hover, mirroring the heading
anchor behaviour.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…rve fix

- CLI heading badges now use colored spans (.cli-badge-ns / .cli-badge-cmd)
  instead of backtick code — purple for namespace, amber for command,
  matching the sidebar nav pill colors. Badges are right-aligned via flex.
- Badge labels read "cli namespace" / "cli command".
- Binary name (schema.Name) is now threaded into CliNamespaceFile and
  CliCommandFile so the --help codeblock and generated usage lines use
  the correct binary name instead of a hardcoded value.
- GenerateUsage: when cmd.Usage is null, auto-generate a usage line with
  required named flags shown explicitly (--flag <flag>) and optional flags
  collapsed to [options].
- FileSystemFactory.InMemoryForPath: new overload that scopes the mock
  write filesystem to the git root of a given path, fixing the scoped
  filesystem access-denied error when `serve --path` points outside the
  docs-builder repo.
- ServeCommand and InMemoryBuildState updated to use InMemoryForPath.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ows "{name} CLI"

- HierarchyCandidates for root now yields "index.md" before "ns-root.md",
  so a cli/index.md in the supplemental folder is picked up as the root
  page body (consistent with how namespace index.md supplementals work).
- ValidateSupplementalFiles no longer errors on root-level index.md;
  it is now validated the same way as any other supplemental index.md
  (error only if unmatched).
- CliRootFile.NavigationTitle overrides to "{schema.Name} CLI" so the
  root entry in the sidebar reads e.g. "elastic CLI" / "docs-builder CLI".

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Mpdreamz Mpdreamz changed the title feat(cli-docs): generate CLI reference docs from argh schema JSON feat(cli-docs): auto-generate CLI reference from schema JSON — namespace cards, deep links, copy-ready usage Apr 30, 2026
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Mpdreamz Mpdreamz changed the title feat(cli-docs): auto-generate CLI reference from schema JSON — namespace cards, deep links, copy-ready usage feat(cli-docs): auto-generate CLI reference from schema JSON Apr 30, 2026
Keep @functions block (nav pills) from this branch and the isTopLevel
index link block added upstream.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…atting

- Regenerate docs/cli-schema.json (upstream added new commands)
- Fix import ordering in DirectiveBlockParser.cs and DirectiveHtmlRenderer.cs
- Fix Prettier formatting in styles.css

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…self-redirects

CLI reference pages are now synthetic (generated from schema) rather than
physical files. The authoring test framework's copyTargetsFromRealDocsIntoMock
was guarded by `File.Exists` which skips synthetic targets, causing redirect
validation to fail for all tests that load the real _redirects.yml.

- Remove the File.Exists guard so stubs are added for all local redirect
  targets; synthetic page validity is checked by the production build
- Remove 9 no-op self-redirects (cli/changelog/*.md → same) and the
  cli/assembler/index.md self-redirect

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
CLI reference pages are volatile by nature (generated from schema) and
were never shipped to production, so their redirects have no value.

- Remove all cli/* entries from _redirects.yml
- Revert Setup.fs to File.Exists guard (unconditional stubs broke other tests)
- Update LinkReferenceFile.fs snapshot to remove stale CLI entries
- Update PhysicalDocsetTests to check for CliReferenceRef instead of
  FolderRef for the cli TOC entry
- Skip synthetic files in Move.ProcessMarkdownFile — they have no physical
  content and ReadAllTextAsync would throw FileNotFoundException

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant