{"slug": "clidoc-brings-tsdoc-style-documentation-to-clis", "title": "CliDoc Brings TSDoc-Style Documentation to CLIs", "summary": "Ben Houston released CliDoc, a Node tool that reads existing CLI command definitions from Yargs, Commander, or oclif and generates an OpenCLI JSON/YAML description, Markdown reference pages for Docusaurus and VitePress, and a `docgen` command that exposes the same description to AI agents. CliDoc implements the OpenCLI specification for Node and is already used by Houston's five command-line tools, including mtlx, node-prewarm, HDRify, git-dedup, and webgpu-bench.", "body_md": "# CliDoc Brings TSDoc-Style Documentation to CLIs\n\nCliDoc generates an OpenCLI description of your CLI from the command definitions you already wrote, then publishes reference docs to Docusaurus and VitePress, and gives AI agents the same description through a docgen command.\n\n[Ben Houston](https://ben3d.ca/about) •  • 7 min read\n\nI build a lot of command-line tools: [mtlx](https://github.com/bhouston/mtlx), [node-prewarm](https://github.com/bhouston/node-prewarm), [HDRify](https://github.com/bhouston/hdrify), [git-dedup](https://ben3d.ca/blog/git-dedup-faster-smaller-checkouts), and [webgpu-bench](https://ben3d.ca/blog/introducing-webgpu-bench). Each one needs reference documentation, and writing it by hand is tedious work that repeats what the command definitions already say.\n\nSo I wrote **[CliDoc](https://clidoc.dev)**. It reads your CLI's command definitions, produces a structured description of the whole tool, and turns that into reference documentation. All five tools above now use it.\n\n## TSDoc, but for CLIs[#](#tsdoc-but-for-clis)\n\nLibrary code solved this years ago. With TSDoc or JSDoc, the comment next to a function becomes its reference page. Web APIs have the same thing in OpenAPI (Swagger): you describe an endpoint once, in code, and get a machine-readable spec, a reference site, client generators, and validators.\n\nCLIs have pieces of this. oclif can generate a README from its commands, and there are man pages and `--help`. But each framework uses its own format, and `--help` is free-form text meant for a person at a terminal. There is no portable, framework-neutral description of a CLI that other tools can build on.\n\nThe [OpenCLI specification](https://github.com/bcdxn/opencli) provides one. It defines a JSON/YAML format for describing a CLI: commands, subcommands, arguments, flags, types, defaults, choices, examples, and exit codes. CliDoc implements OpenCLI for Node.\n\n## How It Works[#](#how-it-works)\n\nclidoc takes your CLI definitions, builds an OpenCLI document, renders that document as Markdown pages, and publishes those pages to your docs site.\n\nThe framework packages read the commands you already wrote:\n\n| Package | Reads | \n|---|---|\n| `@clidoc/yargs` | Yargs command modules | \n| `@clidoc/commander` | Commander command trees | \n| `@clidoc/oclif` | oclif manifests | \n\nThere is no second place to describe a command. Here is the `validate` command from CliDoc itself:\n\n``` js\nexport const command = defineCommand({\n  command: 'validate <input>',\n  describe: 'Validate an OpenCLI JSON or YAML document',\n  builder: (yargs) =>\n    yargs.positional('input', {\n      type: 'string',\n      demandOption: true,\n      describe: 'OpenCLI document filename',\n    }),\n  handler: async ({ input }) => {\n    parse(await readFile(input, 'utf8'));\n    process.stdout.write('Valid OpenCLI document\\n');\n  },\n});\n```\n\nCliDoc turns that into this entry in the OpenCLI document:\n\n```\n\"clidoc validate\": {\n  \"summary\": \"Validate an OpenCLI JSON or YAML document\",\n  \"args\": [\n    {\n      \"name\": \"input\",\n      \"required\": true,\n      \"type\": \"string\",\n      \"summary\": \"OpenCLI document filename\"\n    }\n  ]\n}\n```\n\nand this Markdown page:\n\n```\n## clidoc validate\n\nValidate an OpenCLI JSON or YAML document\n\n### Usage\n\n``` sh\nclidoc validate <input> [--help] [--version]\n```\n\n| Argument | Type | Required | Description |\n| --- | --- | --- | --- |\n| `input` | string | Yes | OpenCLI document filename |\n```\n\n## Why This Matters for Agents[#](#why-this-matters-for-agents)\n\nThe command line is the natural interface for coding agents. Every agent can already run a shell command, so a CLI works as an agent tool without an SDK, a server, or a custom integration.\n\nBut an agent still has to learn the tool. Today that means running `--help`, parsing the output, running `--help` on each subcommand, and hoping the text is complete. It works, but it is slow and loses information.\n\nCliDoc gives agents two better options:\n\n- **Introspection.** Any CliDoc-enabled CLI has a`docgen` command. Running`mycli docgen` prints the full OpenCLI document: every command, argument, flag, type, default, and choice as JSON, in one call.\n- **Online documentation.** The same document renders into a reference site with one page per command, which agents and people can read instead of guessing from help text.\n\nBoth come from the command definitions, so as long as you regenerate them on build, they match the code.\n\n## Adding It to Your CLI[#](#adding-it-to-your-cli)\n\nInstall the packages for your framework. For Yargs:\n\n```\npnpm add @clidoc/yargs @clidoc/core\n```\n\nThen register a `docgen` command that builds the document from the same commands the CLI runs:\n\n``` python\nimport { readFileSync } from 'node:fs';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\nimport { createDocgenCommand, fromYargs } from '@clidoc/yargs';\nimport { infoFromPackageJson, type OpenCliDocument } from '@clidoc/core';\nimport { command as greet } from './commands/greet.js';\n\n// title, binary name, and version come from package.json\nconst pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));\nconst info = infoFromPackageJson(pkg);\n\nconst parser = yargs(hideBin(process.argv)).command(greet);\n\nlet document: OpenCliDocument;\nparser.command(createDocgenCommand(() => document)).demandCommand();\n// build the document once every command, including docgen, is registered\ndocument = fromYargs(parser, info);\n\nparser.parse();\n```\n\nNow your CLI can document itself:\n\n```\nmycli docgen                                         # JSON to stdout\nmycli docgen --output cli.json\nmycli docgen --format markdown --output reference.md\n```\n\n`docgen` is a normal, visible command, listed in `--help` like any other, so an agent exploring your CLI finds it on its own. It builds the document from command metadata and never runs your other command handlers.\n\nCommander works the same way with `fromCommander`. oclif reads the manifest generated at build time, and its `docgen` command comes from the separate `@clidoc/oclif/docgen` entry point, so importing `fromOclif` doesn't require `@oclif/core`. Each framework has a [guide and runnable demo](https://clidoc.dev/docs/frameworks).\n\nI also wrote [yargs-file-commands](https://github.com/bhouston/yargs-file-commands), which puts each Yargs command in its own file. CliDoc supports it directly, including subcommands loaded lazily through async builders.\n\nSome things can't be read from framework metadata, such as examples, exit codes, or the license. Add them with `mergeDocument` inside the function you pass to `docgen`:\n\n``` js\nimport { mergeDocument } from '@clidoc/core';\n\nparser.command(\n  createDocgenCommand(() =>\n    mergeDocument(document, {\n      commands: {\n        'mycli greet': {\n          examples: [{ title: 'Basic', content: 'mycli greet Ada' }],\n          exitCodes: [{ code: 1, status: 'BAD_USER_INPUT_ERROR', summary: 'Missing name' }],\n        },\n      },\n    }),\n  ),\n);\n```\n\n## Publishing to Docusaurus and VitePress[#](#publishing-to-docusaurus-and-vitepress)\n\nOnce you have an OpenCLI document, publishing is a plugin. The output is ordinary Markdown with front matter, plus sidebar entries, so it sits alongside your hand-written guides.\n\nFor Docusaurus, `@clidoc/docusaurus` generates the pages at build time. Set `markdown.format` to `'md'` so Docusaurus doesn't parse generated descriptions as MDX:\n\n```\nmodule.exports = {\n  markdown: { format: 'md' },\n  plugins: [\n    [clidocPlugin, { input: 'cli.json', outputDir: 'docs/generated-cli', basePath: '/cli' }],\n  ],\n};\n```\n\nFor VitePress, `@clidoc/vitepress` writes the pages and returns the matching sidebar:\n\n``` js\nconst cliSidebar = await writeVitePress(document, {\n  outputDir: fileURLToPath(new URL('..', import.meta.url)),\n  basePath: '/cli',\n});\n```\n\nEach command gets its own page with a usage line, an argument table, and a flag table. Regenerate on every build and the reference stays current. The [git-dedup docs](https://git-dedup.ben3d.ca/docs/cli) use the Docusaurus plugin, and CliDoc's own [CLI reference](https://clidoc.dev/docs/cli/reference) is generated from its own commands.\n\n## The Rest of the Toolbox[#](#the-rest-of-the-toolbox)\n\nThe `@clidoc/cli` package (`npm install -g @clidoc/cli`) provides a CLI for working with OpenCLI documents:\n\n- **`clidoc validate`** checks a JSON or YAML document against the OpenCLI schema offline, plus the main logical checks from the upstream Go validator.\n- **`clidoc markdown`** renders a document to Markdown.\n- **`clidoc completion`** generates standalone Bash, Zsh, and Fish completion scripts from a document.\n- **`clidoc mcp`** turns a document into[Model Context Protocol](https://modelcontextprotocol.io) tool definitions and can serve them over stdio, so an MCP client can call your CLI's commands as typed tools. See the[MCP guide](https://clidoc.dev/docs/mcp) .\n\nThere is also a [GitHub Action](https://clidoc.dev/docs/github-action/) that rejects invalid OpenCLI documents in pull requests.\n\nFrom one description you get the reference docs, shell completions, and agent tools for a CLI.\n\n## Dogfooding[#](#dogfooding)\n\nCliDoc documents itself. `clidoc docgen` describes the `clidoc` binary from its own command files, and the reference site is regenerated from that output on every release. Adopting CliDoc across my five tools also surfaced cases the first version didn't handle, such as required variadic flags and lazily loaded Yargs subcommands, which are now supported.\n\n## Try It[#](#try-it)\n\n```\nnpm install -g @clidoc/cli\n```\n\nStart with the [documentation at clidoc.dev](https://clidoc.dev), pick the guide for your framework, and add a `docgen` command. Then run `mycli docgen` and hand the output to your coding agent. The source is on [GitHub](https://github.com/bhouston/clidoc) under the MIT license and requires Node 22.12 or newer.", "url": "https://wpnews.pro/news/clidoc-brings-tsdoc-style-documentation-to-clis", "canonical_source": "https://ben3d.ca/blog/clidoc-openapi-for-clis", "published_at": "2026-09-30 00:00:00+00:00", "updated_at": "2026-09-30 18:21:11.240539+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["CliDoc", "Ben Houston", "OpenCLI", "Yargs", "Commander", "oclif", "Docusaurus", "VitePress"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/clidoc-brings-tsdoc-style-documentation-to-clis", "markdown": "https://wpnews.pro/news/clidoc-brings-tsdoc-style-documentation-to-clis.md", "text": "https://wpnews.pro/news/clidoc-brings-tsdoc-style-documentation-to-clis.txt", "jsonld": "https://wpnews.pro/news/clidoc-brings-tsdoc-style-documentation-to-clis.jsonld"}}