Skip to content
Draft
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
76aa567
docs: add graph command design spec
kanoru3101 Jun 11, 2026
4901e9d
docs: add graph command implementation plan
kanoru3101 Jun 11, 2026
159d367
feat: add dependency graph builder for graph command
kanoru3101 Jun 11, 2026
ec61bfc
docs: sync graph plan with review fixes (slash, ResolveError, typeche…
kanoru3101 Jun 11, 2026
66e168e
feat: add affected-files filter for graph command
kanoru3101 Jun 11, 2026
f0f6022
feat: add graph command output renderers
kanoru3101 Jun 11, 2026
32421fc
docs: reconcile mermaid label escaping in graph spec with implementation
kanoru3101 Jun 11, 2026
f5d3fe3
feat: register graph command in CLI
kanoru3101 Jun 11, 2026
0db7f34
docs: document repeated --affected-by flag syntax in graph spec and plan
kanoru3101 Jun 11, 2026
0bd9972
test: add graph command e2e tests
kanoru3101 Jun 11, 2026
fd71521
docs: document graph command and add changeset
kanoru3101 Jun 11, 2026
fa6d868
docs: align spec warning wording with implementation
kanoru3101 Jun 11, 2026
cb940b7
docs: add tree command rework spec and plan
kanoru3101 Jun 12, 2026
873b7b7
refactor: rename graph command to tree
kanoru3101 Jun 12, 2026
759df76
feat: add pointer-to-node mapping for the tree structure view
kanoru3101 Jun 12, 2026
38a12d4
feat: add internal-structure builder for the tree command
kanoru3101 Jun 12, 2026
9e7c47d
refactor: make stylish summary and empty message caller-provided
kanoru3101 Jun 12, 2026
5013774
feat: match affected-by inputs against tree nodes
kanoru3101 Jun 12, 2026
b20163b
feat: make document structure the default tree view behind --files fa…
kanoru3101 Jun 12, 2026
bcdebfa
test: cover tree structure mode end to end
kanoru3101 Jun 12, 2026
0fb8560
docs: document the tree command and update the changeset
kanoru3101 Jun 12, 2026
8f2e055
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 12, 2026
dcf6a67
fix: hoist entity test imports to avoid per-test transform timeout
kanoru3101 Jun 12, 2026
ff3ad50
docs: unbreak documentation tests for the tree command
kanoru3101 Jun 15, 2026
2b0b60d
refactor(tree): share toNodeId/byString/OPERATION_METHODS via node-id
kanoru3101 Jun 15, 2026
e12ebba
fix: improvments
kanoru3101 Jun 15, 2026
a71529c
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 17, 2026
d925da9
refactor: tidy tree renderer sort and trim a restating comment
kanoru3101 Jun 17, 2026
6607ffc
test: drop redundant tree tests (runtime round-trip, path.resolve var…
kanoru3101 Jun 17, 2026
755baa3
docs: clarify --affected-by per mode and JSON field scope in tree docs
kanoru3101 Jun 17, 2026
f87eb4d
test: cover multi-API rejection in default view and multi-API --files…
kanoru3101 Jun 17, 2026
6348a8e
fix: tidy tree command — share mode context, dedupe location mapping,…
kanoru3101 Jun 17, 2026
b46857f
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 17, 2026
7efffd9
fix: remove comments
kanoru3101 Jun 17, 2026
655dc5e
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 22, 2026
3e690f6
fix: refactoring
kanoru3101 Jun 22, 2026
e411931
fix: resolve bug with splitted files
kanoru3101 Jun 22, 2026
3273609
fix: update tests
kanoru3101 Jun 22, 2026
d6e78f8
Apply suggestions from code review
JLekawa Jun 23, 2026
fb2f569
Update tree.md
JLekawa Jun 23, 2026
7aa745b
Update docs/@v2/commands/tree.md
JLekawa Jun 23, 2026
94256c8
docs(cli): fix issues in file
JLekawa Jun 23, 2026
9c02500
Apply suggestion from @JLekawa
JLekawa Jun 23, 2026
c460414
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 23, 2026
f8f3f83
fix: add spaces in the options-table separator
kanoru3101 Jun 23, 2026
1df3366
fix: refactoring and update snapshots
kanoru3101 Jun 24, 2026
3738f2a
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 24, 2026
820d75e
fix: update docs
kanoru3101 Jun 24, 2026
8917cf0
Merge branches 'feat/graph-command' and 'feat/graph-command' of githu…
kanoru3101 Jun 24, 2026
1f161b5
fix: resolve cursor bugs
kanoru3101 Jun 24, 2026
e111d75
fix: update docs
kanoru3101 Jun 24, 2026
8bef8af
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 25, 2026
dce5f2f
chore: small fixes
kanoru3101 Jun 26, 2026
4cdd016
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 1, 2026
64fb2dd
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 2, 2026
dda77cf
feat: add level flag
kanoru3101 Jul 13, 2026
96a6cbf
test: add snapshot
kanoru3101 Jul 13, 2026
a5bfe1f
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 13, 2026
40ea0d7
feat(tree): show operationId on operation nodes
kanoru3101 Jul 16, 2026
d7c39f7
docs(tree): fix options table having a stray fourth column
kanoru3101 Jul 17, 2026
2bfcd8c
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 17, 2026
71bbe5e
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 27, 2026
b9a4953
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 28, 2026
562b9cf
refactor(tree): move the engine to core and walk resolved files with …
kanoru3101 Aug 3, 2026
0ff91b5
Merge branch 'main' into feat/graph-command
kanoru3101 Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/graph-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/cli': minor
---

Added the `tree` command that displays the structure of an API description — its paths, operations, and component dependency chains.
1 change: 1 addition & 0 deletions docs/@v2/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ API management commands:
- [`score`](score.md) Score an API for integration simplicity and AI agent readiness.
- [`split`](split.md) Split API description into a multi-file structure.
- [`stats`](stats.md) Gather statistics for a document.
- [`tree`](tree.md) Display the structure of an API description as a tree.

Linting commands:

Expand Down
179 changes: 179 additions & 0 deletions docs/@v2/commands/tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# `tree`

## Introduction

The `tree` command prints the structure of an API description: its paths, operations, and the component dependency chains between them through `$ref`.
The default view bundles the description first, so a multi-file API shows the same full tree as its single-file form.
The command works fully with OpenAPI 2.0 and 3.x.
AsyncAPI and Arazzo descriptions are supported too, but render as a flat list of their top-level referenced (`$ref`) components rather than a paths and operations tree.

Use `tree` to:

- Get quick orientation in any API, whether single-file or multi-file.
- Run impact analysis with `--used-by` — which paths and operations use a given component or file.
This analysis is useful in CI and automated code review.
- Produce machine-readable JSON, a Mermaid diagram, or a Graphviz DOT graph with `--format`.
- View the file-level `$ref` graph with `--files`.

## Usage

```bash
redocly tree
redocly tree <api>
redocly tree <api> [--format=<value>] [--used-by=<value>] [--output=<file>] [--config=<path>]
redocly tree --files [apis...]
```

With no API argument, the command takes the API from the Redocly configuration file.
The default structure view displays one API at a time.
Use `--files` for the multi-API file graph.

## Options

| Option | Type | Description |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apis | [string] | In default mode, exactly one API description file or alias. In `--files` mode, one or more files or aliases. Defaults to APIs from the Redocly configuration file. |
| --config | string | Specify the path to the [Redocly configuration file](../configuration/index.md). |
| --files | boolean | Display the file-level `$ref` graph instead of the document structure. |
| --format | string | Output format: `stylish` (default, tree view), `json`, `mermaid`, or `dot`. |
| --help | boolean | Display help. |
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
| --output, -o | string | Write the output to a file instead of `stdout`. |
| --used-by | [string] | Display only the part of the tree that uses (depends on) the given components, paths, or files. The default view accepts a JSON pointer, shorthand pointer, bare component name, or file path. `--files` mode accepts file paths only. Repeat the option to pass several values. |
| --version | boolean | Display version number. |

## Examples

### Print the structure of an API description

```bash
redocly tree cafe.yaml
```

```treeview
cafe.yaml
├── /menu
│ └── GET
│ ├── responses/BadRequest
│ │ └── schemas/Error
│ └── schemas/MenuItemList
│ ├── schemas/MenuItem
│ │ ├── schemas/Beverage
│ │ │ └── schemas/MenuBaseItem
│ │ └── schemas/Dessert
│ │ └── schemas/MenuBaseItem
│ └── schemas/Page
├── /orders
│ ├── GET
│ │ └── schemas/OrderList
│ │ └── schemas/Order
│ └── POST
│ └── schemas/Order
└── … (other paths)
```

The tree above is abbreviated for readability — shared parameters and the repeated error responses are omitted.
An operation is shown as the method only (`GET`) under its path, since the path is its parent.

Markers legend:

- `↺` — a cycle: the node references one of its ancestors (a recursive schema). It is not expanded again. A node that simply appears in more than one place (fan-in) is shown without a marker.
- `✗ not found` — an unresolvable `$ref` (only in `--files` mode; in the default view an unresolvable `$ref` is an error, see below)
- `(external)` — a reference to a URL

The default view bundles the description, so components and operations split across files are resolved to their canonical place.
A multi-file API therefore produces the same tree as its single-file equivalent — operations and named components, not file nodes.

### Find what uses a component, path, or file

Pass one or more components, paths, or files to `--used-by` to see only the part of the tree that depends on them:

```bash
redocly tree cafe.yaml --used-by schemas/Order
```

```treeview
cafe.yaml
├── /orders
│ ├── GET
│ │ └── schemas/OrderList
│ │ └── schemas/Order│ └── POST
│ └── schemas/Order└── /orders/{orderId}
├── GET
│ └── schemas/Order └── PATCH
└── schemas/Order
4 of 12 operations affected · affected paths: /orders, /orders/{orderId}
```

`--used-by` accepts several input forms:

- full JSON pointer: `#/components/schemas/Order`
- shorthand pointer (the node id): `schemas/Order`
- bare component name: `Order` — ambiguous bare names match all candidates and print a note to `stderr`
- a file path (in `--files` mode): `components/schemas/Order.yaml`
- the root file itself: the whole tree is affected

The summary line reports how many operations are affected.
A change that only affects path-level parameters can report `0 of N operations affected` while still listing the affected path: the path itself is impacted, not its operations.
For AsyncAPI or Arazzo descriptions, which have no operation nodes, the summary counts nodes instead — for example, `5 of 8 nodes affected`.

A file path that matches no node prints a warning and points you to `--files`; other unknown inputs print a warning. Both exit with code `0`.

### Machine-readable output

```bash
redocly tree cafe.yaml --format=json
```

Prints the graph as JSON in the common `nodes`/`links` shape (compatible with D3, force-graph, and similar tools). Every node carries `resolved` and `external`; `kind` and `file` are present in the default view. Each link carries the exact `$ref` strings.

```bash
redocly tree cafe.yaml --format=mermaid
```

Prints a [Mermaid](https://mermaid.js.org/) `flowchart` definition.

```bash
redocly tree cafe.yaml --format=dot
```

Prints a [Graphviz](https://graphviz.org/) `digraph`, consumable by Graphviz and most graph-drawing tools.

### Write the output to a file

Use `--output` (`-o`) to write any format to a file instead of `stdout`:

```bash
redocly tree cafe.yaml --format=mermaid --output cafe.mmd
```

### Invalid descriptions

The default view bundles the description before walking it.
If the description cannot be bundled — for example, it has unresolvable or invalid `$ref`s — `tree` prints the bundling problems and exits with a non-zero code instead of printing a partial tree.

### File-level graph

```bash
redocly tree cafe.yaml --files
```

```treeview
cafe.yaml
├── paths/menu.yaml
│ ├── components/parameters/Limit.yaml
│ ├── components/responses/BadRequest.yaml
│ │ └── components/schemas/Error.yaml
│ └── components/schemas/MenuItemList.yaml
│ └── components/schemas/MenuItem.yaml
└── paths/orders.yaml
└── components/schemas/OrderList.yaml
└── components/schemas/Order.yaml
```

The tree above is abbreviated; the real output lists every file.
`--files` displays only which files reference other files — not the paths, operations, and components inside them.
Paths are shown relative to the directory of the root description, so the folder you run the command from does not appear as a prefix.
The default view already traverses those elements, following `$ref`s across files.
`--files` also accepts multiple APIs in one run, merging their graphs.
In this mode, `--used-by` takes file paths, and the summary counts affected files and roots.
2 changes: 2 additions & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
page: commands/stats.md
- label: translate
page: commands/translate.md
- label: tree
page: commands/tree.md
- group: Guides
page: guides/index.md
items:
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ export async function handleLintConfig(argv: Exact<CommandArgv>, version: string
return;
}

if (argv.format === 'json' || argv.format === 'junit' || argv.format === 'checkstyle') {
if (
argv.format === 'json' ||
argv.format === 'junit' ||
argv.format === 'checkstyle' ||
argv.format === 'mermaid' ||
argv.format === 'dot'
) {
// these are single-document formats, so a separate config-lint document would break the output
return;
}
Expand Down
163 changes: 163 additions & 0 deletions packages/cli/src/commands/tree/__tests__/build-graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ResolveError, Source, type Document, type ResolvedRefMap } from '@redocly/openapi-core';
import * as path from 'node:path';

import { buildGraph } from '../build-graph.js';

const CWD = '/project';

function makeDocument(absoluteRef: string): Document {
return { source: new Source(absoluteRef, ''), parsed: {} };
}

function resolvedEntry(targetAbsoluteRef: string, isRemote = true) {
return {
resolved: true as const,
isRemote,
node: {},
nodePointer: '#/',
document: makeDocument(targetAbsoluteRef),
};
}

const resolveRef = (base: string, uri: string) => path.resolve(path.dirname(base), uri);

describe('buildGraph', () => {
it('builds nodes and edges from cross-file refs, transitively', () => {
const refMap: ResolvedRefMap = new Map([
['/project/openapi.yaml::paths/users.yaml', resolvedEntry('/project/paths/users.yaml')],
[
'/project/paths/users.yaml::../components/User.yaml',
resolvedEntry('/project/components/User.yaml'),
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph).toEqual({
roots: ['openapi.yaml'],
nodes: [
{ id: 'components/User.yaml', resolved: true },
{ id: 'openapi.yaml', root: true, resolved: true },
{ id: 'paths/users.yaml', resolved: true },
],
edges: [
{ from: 'openapi.yaml', to: 'paths/users.yaml', refs: ['paths/users.yaml'] },
{
from: 'paths/users.yaml',
to: 'components/User.yaml',
refs: ['../components/User.yaml'],
},
],
});
});

it('skips same-file refs', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::#/components/schemas/Pet',
{ ...resolvedEntry('/project/openapi.yaml'), isRemote: false },
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([{ id: 'openapi.yaml', root: true, resolved: true }]);
expect(graph.edges).toEqual([]);
});

it('dedupes edges across refs and across roots, collecting distinct sorted refs', () => {
const entryY = resolvedEntry('/project/b.yaml');
const entryX = resolvedEntry('/project/b.yaml');
const refMapA: ResolvedRefMap = new Map([
['/project/a.yaml::b.yaml#/Y', entryY],
['/project/a.yaml::b.yaml#/X', entryX],
]);
const refMapB: ResolvedRefMap = new Map([['/project/a.yaml::b.yaml#/X', entryX]]);

const graph = buildGraph(
[
{ rootDocument: makeDocument('/project/a.yaml'), refMap: refMapA },
{ rootDocument: makeDocument('/project/b.yaml'), refMap: refMapB },
],
{ base: CWD, resolveRef }
);

expect(graph.roots).toEqual(['a.yaml', 'b.yaml']);
expect(graph.edges).toEqual([
{ from: 'a.yaml', to: 'b.yaml', refs: ['b.yaml#/X', 'b.yaml#/Y'] },
]);
expect(graph.nodes).toEqual([
{ id: 'a.yaml', root: true, resolved: true },
{ id: 'b.yaml', root: true, resolved: true },
]);
});

it('represents unresolved refs as resolved:false nodes with an edge', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::./missing.yaml#/Pet',
{
resolved: false as const,
isRemote: true,
document: undefined,
error: new ResolveError(new Error('ENOENT')),
},
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([
{ id: 'missing.yaml', resolved: false },
{ id: 'openapi.yaml', root: true, resolved: true },
]);
expect(graph.edges).toEqual([
{ from: 'openapi.yaml', to: 'missing.yaml', refs: ['./missing.yaml#/Pet'] },
]);
});

it('keeps http(s) targets as external URL nodes', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::https://example.com/shared.yaml#/S',
resolvedEntry('https://example.com/shared.yaml'),
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([
{ id: 'https://example.com/shared.yaml', external: true, resolved: true },
{ id: 'openapi.yaml', root: true, resolved: true },
]);
});

it('handles cyclic file references', () => {
const refMap: ResolvedRefMap = new Map([
['/project/a.yaml::b.yaml', resolvedEntry('/project/b.yaml')],
['/project/b.yaml::a.yaml', resolvedEntry('/project/a.yaml')],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/a.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.edges).toEqual([
{ from: 'a.yaml', to: 'b.yaml', refs: ['b.yaml'] },
{ from: 'b.yaml', to: 'a.yaml', refs: ['a.yaml'] },
]);
});
});
Loading
Loading