# Create CLI Command From API Method

> Adds a dx-cli subcommand from a DX Web API method docs page—fetch docs, stub Commander hierarchy, implement request/options, verify with make and --help. Use when the user provides or wants a https://docs.getdx.com/webapi/methods/ URL, asks to scaffold or implement a CLI command for a Web API method, or says "create command from API".

- Skill: `get-dx/create-cli-command-from-api-method` (Agent Skill)
- Install (CLI): `npx skillmds@latest add get-dx/create-cli-command-from-api-method`
- Raw SKILL.md: https://api.skillmd.com/api/skills/get-dx/create-cli-command-from-api-method/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: get-dx (https://skillmd.com/u/get-dx)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/get-dx/create-cli-command-from-api-method

---


# Create CLI command from Web API method

End-to-end workflow for wiring a new `dx` subcommand to a documented Web API method in the **dx-cli** repo.

## Step 1: Get the docs page

1. **Require a docs URL from the user.** It must start with `https://docs.getdx.com/webapi/methods/`. If they did not give one (or it does not match), ask for a valid URL and stop until you have it.
2. **Fetch the page HTML or rendered content** (for example `curl` with `Accept: text/html`, or another reliable fetch). Parse the **Facts** table (HTTP method, path), **Arguments** (required vs optional, types, descriptions), **Example request/response**, and **Errors** from the body.

**Naming note:** The URL path uses an all-lowercase method slug (e.g. `catalog.entitytypes.list`). The docs **title** and the real API method name use camelCase in each segment after the first (e.g. `catalog.entityTypes.list`). Use the **canonical method spelling from the docs title/Facts** for HTTP paths and CLI segment names, not only the URL slug.

**HTTP route string:** The path passed to `request()` must match the **Facts** table exactly (e.g. `/catalog.entities.list`, `/catalog.entityTypes.list`). When the documented path is all lowercase, use that literal string—do not invent camelCase inside the path unless the docs show it.

## Step 2: Map method name → CLI hierarchy and files

Split the method name on `.` (e.g. `catalog.entityTypes.list` → `["catalog", "entityTypes", "list"]`).

- The **first** segment is the top-level command group (e.g. `catalog`). It is registered from `src/cli.ts` via `src/commands/<first>.ts` (e.g. `catalog.ts`).
- Each **middle** segment is a nested `Command` (e.g. `entities`, `entityTypes`). Prefer one module per middle group under `src/commands/<first>/` (e.g. `src/commands/catalog/entities.ts` exporting `entitiesCommand()`).
- The **last** segment is the leaf subcommand (e.g. `info`, `list`).

**CLI invocation shape:** `dx <segment1> <segment2> … <leaf> [args] [options]`
Example: `catalog.entityTypes.list` → `dx catalog entityTypes list`.

**Where to edit:**

- Add or extend the parent command module (e.g. `src/commands/catalog/entityTypes.ts`) and **wire it** from `src/commands/catalog.ts` with `addCommand(...)` if the parent group is new.
- Define the leaf with `.command("<leaf>")`, `.description(...)`, and help text.

**Verify before Step 3:**

```bash
make
dx <segment1> <segment2> … <leaf> --help
```

If `--help` does not show the expected description or `afterAll` examples (once added), fix registration or Commander options until it does.

**If `make` fails:** `make` may run `pnpm install` (use `CI=true make` when there is no TTY) and `pnpm link --global` (can fail without write access to the pnpm home directory). You can still verify the implementation with `pnpm exec tsc -p tsconfig.json` (or `pnpm build`) and `node dist/index.js <segment1> … --help` from the repo root.

## Step 3: Implement the command

### Arguments vs options

- Use the docs **Arguments** section: optional API parameters → Commander **options** (`.option(...)`). Use names and descriptions aligned with the docs.
- If there is a single **critical** required parameter such as `id` or `identifier` (or the docs clearly treat one value as the primary resource key), expose it as a required **argument** (`.argument("<name>", "...")`), not as `--name`.
- **List/get methods with no resource key:** If every API parameter is optional (e.g. pagination and filters only), the leaf command has **no** `.argument(...)`. The action signature is `wrapAction(async (options, command) => { … })`—there is no leading positional parameter before `options`.
- **API query names vs CLI flags:** Query string keys sent to `request()` must use the API’s spelling (often `snake_case`, e.g. `search_term`). Commander long options are usually `kebab-case` (e.g. `--search-term`). Commander maps those to **camelCase** properties on the options object (e.g. `searchTerm`). Map explicitly in the handler: `search_term: options.searchTerm` (and omit keys when the value is `undefined` so server defaults still apply).
- **Integer options:** For counts and limits, use a value parser on `.option(...)` that returns a number and throws **`CliError`** with **`EXIT_CODES.ARGUMENT_ERROR`** when the value is not a valid positive integer (see **`parsePositiveIntOption`** in `src/commands/catalog/entities.ts`).

### HTTP and API layer

- Add a small typed (or pragmatically typed) function in the same module as the command that calls `request()` from `src/http.ts` with the correct **method**, **path**, **query**, and **body** as in the docs **Facts** / **Example request**.
- The API response typically has one field that is the most important, like `entity` for `GetEntityTypeResponse`. Define or reuse a named type to represent this object rather than `unknown`. If the shape is unclear, search on the [API object types](https://docs.getdx.com/webapi/object-types/) page to get more information.
- Path shape matches the API: leading slash + method name with dots, e.g. `/catalog.entities.info`, `/catalog.entityTypes.list` (match the documented path; preserve camelCase as in the official method name when the docs show it).
- Build the `query` object in the API helper: only set keys the caller provided; leave omitted keys out of the query object so optional parameters default on the server (e.g. do not send `limit` unless the user passed `--limit`).

### Action handler

- Use **`buildRuntime`** and **`getContext(command)`** from existing commands (see `src/commands/catalog/entities.ts`).
- Wrap the async action with **`wrapAction`** from `src/commandHelpers.ts` so errors are handled consistently.
- **Rendering:** branch on the `--json` flag:
  - `--json` present → call **`renderJson(response)`** from `src/renderers.ts`.
  - `--json` absent → build an array of `Block` objects using helpers from **`src/ui.ts`**, then pass the array to **`renderRichText(blocks)`** from `src/renderers.ts`.

### Examples in help

- Add **2–3** examples via **`.addHelpText("afterAll", createExampleText([...]))`** from `src/commandHelpers.ts`, following **`catalog entities`** commands in `src/commands/catalog/entities.ts` (`label` + full `dx ...` command line including `--json` where useful). For list/pagination methods, include an example that shows **`--cursor`** (or the doc’s pagination param) using a placeholder value from the docs example response if available.

### Types and validation

- Reuse **`CliError`** / **`EXIT_CODES`** from `src/errors.ts` for invalid user input (e.g. bad enum, missing combinations).
- Follow existing import style: **`commander`** `Command`, `.js` extensions on local imports.

## Step 4: Add tests

Add tests in the co-located test file `src/commands/<group>/<subcommand>.test.ts` (create it if it does not exist). Follow the patterns in existing test files such as `src/commands/catalog/entities.test.ts` and `src/commands/catalog/entityTypes.test.ts`. At minimum, cover:

- **Happy path:** mock the HTTP layer and assert the command exits `0` and produces the expected output.
- **`--json` flag:** assert the output is valid JSON matching the mocked response shape.
- **Argument/option validation errors:** assert `CliError` is thrown (or the process exits non-zero) for invalid input (e.g. bad `--limit`, invalid enum value).

Run `pnpm test` after adding tests to confirm they pass.

## Checklist

- [ ] Docs URL validated and content fetched.
- [ ] Method name casing correct (title/API, not only URL slug); HTTP path matches **Facts** table.
- [ ] Parent subcommands exist and are wired in `src/commands/<top>.ts`.
- [ ] Build succeeds and `dx … --help` shows description and examples (`make`, or `pnpm build` / `tsc` + `node dist/index.js` if `make` fails on link/install).
- [ ] API call matches docs; action uses `wrapAction`, `renderJson` (for `--json`), and `renderRichText` with `ui.ts` blocks (for human output).
- [ ] Required resource key is a positional argument when appropriate; other params are options; snake_case query params map correctly from Commander options.
- [ ] Co-located test file exists with happy path, `--json`, validation error, and missing-auth cases; `pnpm test` passes.

