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
- 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.
- 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:
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 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
1---2name: create-cli-command-from-api-method3description: 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".4---5
6# Create CLI command from Web API method
7
8End-to-end workflow for wiring a new `dx` subcommand to a documented Web API method in the **dx-cli** repo.
9
10## Step 1: Get the docs page
11
121. **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.
132. **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.
14
15**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.
16
17**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.
18
19## Step 2: Map method name → CLI hierarchy and files
20
21Split the method name on `.` (e.g. `catalog.entityTypes.list` → `["catalog", "entityTypes", "list"]`).
22
23- 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`).
24- 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()`).
25- The **last** segment is the leaf subcommand (e.g. `info`, `list`).
26
27**CLI invocation shape:** `dx <segment1> <segment2> … <leaf> [args] [options]`
28Example: `catalog.entityTypes.list` → `dx catalog entityTypes list`.
29
30**Where to edit:**
31
32- 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.
33- Define the leaf with `.command("<leaf>")`, `.description(...)`, and help text.
34
35**Verify before Step 3:**
36
37```bash
38make
39dx <segment1> <segment2> … <leaf> --help
40```
41
42If `--help` does not show the expected description or `afterAll` examples (once added), fix registration or Commander options until it does.
43
44**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.
45
46## Step 3: Implement the command
47
48### Arguments vs options
49
50- Use the docs **Arguments** section: optional API parameters → Commander **options** (`.option(...)`). Use names and descriptions aligned with the docs.
51- 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`.
52- **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`.
53- **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).
54- **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`).
55
56### HTTP and API layer
57
58- 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**.
59- 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.
60- 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).
61- 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`).
62
63### Action handler
64
65- Use **`buildRuntime`** and **`getContext(command)`** from existing commands (see `src/commands/catalog/entities.ts`).
66- Wrap the async action with **`wrapAction`** from `src/commandHelpers.ts` so errors are handled consistently.
67- **Rendering:** branch on the `--json` flag:
68 - `--json` present → call **`renderJson(response)`** from `src/renderers.ts`.
69 - `--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`.
70
71### Examples in help
72
73- 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.
74
75### Types and validation
76
77- Reuse **`CliError`** / **`EXIT_CODES`** from `src/errors.ts` for invalid user input (e.g. bad enum, missing combinations).
78- Follow existing import style: **`commander`** `Command`, `.js` extensions on local imports.
79
80## Step 4: Add tests
81
82Add 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:
83
84- **Happy path:** mock the HTTP layer and assert the command exits `0` and produces the expected output.
85- **`--json` flag:** assert the output is valid JSON matching the mocked response shape.
86- **Argument/option validation errors:** assert `CliError` is thrown (or the process exits non-zero) for invalid input (e.g. bad `--limit`, invalid enum value).
87
88Run `pnpm test` after adding tests to confirm they pass.
89
90## Checklist
91
92- [ ] Docs URL validated and content fetched.
93- [ ] Method name casing correct (title/API, not only URL slug); HTTP path matches **Facts** table.
94- [ ] Parent subcommands exist and are wired in `src/commands/<top>.ts`.
95- [ ] Build succeeds and `dx … --help` shows description and examples (`make`, or `pnpm build` / `tsc` + `node dist/index.js` if `make` fails on link/install).
96- [ ] API call matches docs; action uses `wrapAction`, `renderJson` (for `--json`), and `renderRichText` with `ui.ts` blocks (for human output).
97- [ ] Required resource key is a positional argument when appropriate; other params are options; snake_case query params map correctly from Commander options.
98- [ ] Co-located test file exists with happy path, `--json`, validation error, and missing-auth cases; `pnpm test` passes.